needlogin = true;
$me = new User();
$me->loadMe();
$this->me = $me;
$this->layout()->set("me", $me);
if (!$me->is(["Admin", "netowner", "lineplanner", "pipeplanner", "pipeworker", "lineworker"])) {
$this->redirect("Dashboard");
}
if ($this->me->is(["Admin"])) {
$this->allowedPops = null;
} else {
$networkIds = array_column($this->me->getProperty('my_networks'), 'id');
$pops = PopNetworkModel::search(['Networks' => $networkIds]);
foreach ($pops as $pop) {
$popIds[] = $pop->pop_id;
}
$this->allowedPops = $popIds;
}
}
protected function indexAction()
{
$deviceManufacturers = array_map(function ($deviceManufacturer) {
return [
"id" => $deviceManufacturer->id,
"name" => $deviceManufacturer->name,
"creator" => $deviceManufacturer->creator->name,
"created" => $deviceManufacturer->create,
];
}, DevicemanufactorModel::getAll());
$deviceTypes = array_map(function ($deviceType) {
return [
"id" => $deviceType->id,
"name" => $deviceType->name,
"manufacturer" => $deviceType->devicemanufactor->name,
"price" => $deviceType->price,
"power" => $deviceType->power,
"temp_warning" => $deviceType->temp_warning,
"temp_critical" => $deviceType->temp_critical,
"creator" => $deviceType->creator->name,
"created" => $deviceType->create,
];
}, DevicetypeModel::getAll());
$this->layout()->set('additionalJS', [
"plugins/chart.js/chart.4.4.6.js",
"plugins/chart.js/chartjs-adapter-moment.min.js",
"plugins/chart.js/chartjs-plugin-zoom.min.js"
]);
$JSGlobals = ["BASE_URL" => self::getUrl(""),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Devices",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Devices", "href" => self::getUrl("Device")]
],
"DEVICE_MANUFACTURERS" => $deviceManufacturers,
"DEVICE_TYPES" => $deviceTypes,
"DEVICES" => $this->getDevices(),
"IS_ADMIN" => $this->me->is(["Admin"]),
"ZABBIX_URL" => (defined("ZABBIX_URL")) ? ZABBIX_URL : "",
"GRAFANA_URL" => (defined("GRAFANA_URL")) ? GRAFANA_URL : "",
];
$this->layout()->set("vueViewName", "Device");
$this->layout()->set("JSGlobals", $JSGlobals);
$this->layout()->setTemplate("VueViews/Vue");
}
protected function detailAction()
{
$id = $this->request->id;
if (!is_numeric($id) || !$id) {
$this->layout()->setFlash("Gerät nicht gefunden", "error");
$this->redirect("Device");
}
$device = new Device($id);
if ($device->id != $id) {
$this->layout()->setFlash("Gerät nicht gefunden", "error");
$this->redirect("Device");
}
if (!$this->me->is(["Admin"])) {
if (!in_array($device->pop_id, $this->allowedPops)) {
$this->layout()->setFlash("Gerät nicht gefunden", "error");
$this->redirect("Device");
}
}
$this->layout()->setTemplate("Device/Detail");
$devicesconfig = DeviceModel::getconifg($id);
$devices = DeviceModel::getOne($id);
$devicesall = DeviceModel::getAll();
if ($devices->devicetype->olt == "1") {
$customer = DeviceModel::getOltCustomer($device->ip);
} else {
$customer = [];
}
$this->layout()->set("devicesconfig", $devicesconfig);
$this->layout()->set("devices", $devices);
$this->layout()->set("devicesall", $devicesall);
$this->layout()->set("customer", $customer);
}
protected function addAction()
{
$this->layout()->setTemplate("Device/Form");
$this->layout()->set("devicetypes", DevicetypeModel::getAll());
$this->layout()->set("pops", PopModel::getAll());
$this->layout()->set("devices", DeviceModel::getAll());
}
protected function editAction()
{
$id = $this->request->id;
if (!is_numeric($id) || !$id) {
$this->layout()->setFlash("Device nicht gefunden", "error");
$this->redirect("Device");
}
$device = new Device($id);
if ($device->id != $id) {
$this->layout()->setFlash("Device nicht gefunden", "error");
$this->redirect("Device");
}
$this->layout()->set("device", $device);
$this->addAction();
}
protected function saveAction()
{
if (!$this->me->is(["Admin"])) {
$this->layout()->setFlash("Keine Berechtigung", "error");
$this->redirect("Device");
}
$r = $this->request;
$id = $r->id;
//var_dump($r->get());exit;
if (is_numeric($id) && $id > 0) {
$mode = "edit";
$device = new Device($id);
if (!$device->id) {
$this->layout()->setFlash("Device nicht gefunden", "error");
$this->redirect("Device");
}
} else {
$mode = "add";
}
$data = [];
$data['name'] = trim($r->name);
$data['devicetype_id'] = $r->devicetype_id;
$data['parent_id'] = $r->parent_id;
$data['autobackup'] = trim($r->autobackup);
$data['backup_check'] = trim($r->backup_check);
if (trim($r->pop_id) == "0") {
$data['pop_id'] = NULL;
} else {
$data['pop_id'] = $r->pop_id;
}
if (!(trim($r->addr_street))) {
$data['addr_street'] = NULL;
$data['addr_number'] = NULL;
$data['addr_extended'] = NULL;
$data['addr_zip'] = NULL;
$data['addr_city'] = NULL;
} else {
$data['addr_street'] = $r->addr_street;
$data['addr_number'] = $r->addr_number;
$data['addr_extended'] = $r->addr_extended;
$data['addr_zip'] = $r->addr_zip;
$data['addr_city'] = $r->addr_city;
}
if (!trim($r->gps_lat) || !trim($r->gps_long)) {
$data['gps_lat'] = NULL;
$data['gps_long'] = NULL;
} else {
$data['gps_lat'] = $r->gps_lat;
$data['gps_long'] = $r->gps_long;
}
if ($data['autobackup'] != "1") {
$data['autobackup'] = "0";
}
if ($data['backup_check'] != "1") {
$data['backup_check'] = "0";
}
if (!$data['parent_id']) {
$data['parent_id'] = NULL;
}
$data['ip'] = $r->ip;
$data['mac'] = $r->mac;
$data['serial'] = $r->serial;
if ($r->snmp_version) {
$data['snmp_version'] = $r->snmp_version;
} else {
$data['snmp_version'] = NULL;
}
if (empty(trim($r->price))) {
$data['price'] = "0.00";
} else {
$data['price'] = $r->price;
}
if (empty(trim($r->power))) {
$data['power'] = "0.0";
} else {
$data['power'] = $r->power;
}
$data['comment'] = $r->comment;
$ipv4_validation_regex = "/^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/";
if (!$data['name']) {
$this->layout()->setFlash("Name darf nicht leer sein", "error");
$this->redirect("Device");
}
if (!$data['ip']) {
$this->layout()->setFlash("IP-Adresse darf nicht leer sein", "error");
$this->redirect("Device");
} else if (!preg_match($ipv4_validation_regex, trim($data['ip']))) {
$this->layout()->setFlash("IP-Adresse ist nicht gültig", "error");
$this->redirect("Device");
}
// var_dump($_FILES);
// var_dump($upload);
// exit;
if ($mode == "edit") {
$device->update($data);
} else {
$device = DeviceModel::create($data);
}
// var_dump($device);
// exit;
$id = $device->save();
$returnUrl = "Device";
$returnAction = "Index";
$returnVariables = array();
$returnAnker = "";
if ($this->request->returnto) {
if (strpos($this->request->returnto, "-") !== false) {
$urls = explode('-', $this->request->returnto);
$urlCounter = 0;
$returnUrlGen = "";
foreach ($urls as $url) {
if ($urlCounter > 0) {
$returnUrlGen .= "/";
}
$returnUrlGen .= ucfirst($url);
$urlCounter++;
}
$returnAction = "";
$returnVariables['id'] = $id;
$returnUrl = $returnUrlGen;
} else {
$returnUrl = ucfirst($this->request->returnto);
}
}
if (!$id) {
$returnVariables['id'] = $r->id;
if ($mode == "edit") {
$this->layout()->setFlash("Device konnte nicht gespeichert werden", "error");
} else if ($mode = "add") {
$this->layout()->setFlash("Device konnte nicht angelegt werden", "error");
}
$this->redirect($returnUrl, $returnAction, $returnVariables, $returnAnker);
}
if ($mode == "edit") {
$this->layout()->setFlash("Device erfolgreich geändert", "success");
} else if ($mode = "add") {
$this->layout()->setFlash("Device erfolgreich angelegt", "success");
}
$this->redirect($returnUrl, $returnAction, $returnVariables, $returnAnker);
}
protected function apiAction()
{
if (!$this->me->is(["Admin"])) {
$return = false;
}
$do = $this->request->do;
$format = $this->request->format;
$filename = $this->request->filename;
$id = $this->request->id;
$ip = $this->request->ip;
$portid = $this->request->portid;
$ports = $this->request->ports;
$adv = $this->request->adv;
$ont = $this->request->ont;
$data = [];
switch ($do) {
case "getDevices":
header('Content-Type: application/json');
die(json_encode($this->getDevices()));
case "getconfig":
$this->getConfig($id, $format, $filename);
break;
case "createconfig":
$this->createConfig($ip);
break;
case "uploadFile":
$this->uploadFile($id);
break;
case "getoltinfo":
$this->getoltInfo($ip, $portid, $adv);
break;
case "getontinfo":
$this->getontInfo($ip, $portid, $ont);
break;
case "getontinfomac":
$this->getontInfoMac($ip, $portid, $ont);
break;
case "changeoltsplitter":
$this->changeoltSplitter($id, $portid, $ports);
break;
case "deviceoltserviceporttimestamp":
$this->deviceoltserviceporttimestamp($ip);
break;
case "deviceoltserviceportrefresh":
$this->deviceoltserviceportrefresh($ip);
break;
case "getZabbixConsolidationData":
$this->getZabbixConsolidationData();
break;
case "getZabbixTemplates":
$this->getZabbixTemplates();
break;
case "createZabbixHost":
$this->createZabbixHost();
break;
case "updateZabbixCoordinates":
$this->updateZabbixCoordinates();
break;
default:
$return = false;
}
}
protected function deleteAction()
{
$id = $this->request->id;
$device = new Device($id);
if (!$device->id || $device->id != $id) {
$this->layout()->setFlash("Gerätetyp nicht gefunden.", "error");
$this->redirect("Device");
}
$device->delete();
$this->redirect("Device");
}
private function getConfig($id, $format, $filename)
{
$configDownload = DeviceModel::getconifgdownload($id, $format);
//
// header('Content-Type: application/octet-stream');
header('Content-Type: text/plain');
header('Content-disposition: attachment; filename="' . $filename . '"');
echo $configDownload;
exit;
}
private function uploadFile($id)
{
$r = $this->request;
$file = $_FILES;
$response = DeviceModel::uploadFile($id, $file);
echo $response;
die();
}
private function createConfig($ip)
{
$r = $this->request;
$id = $r->id;
$createConfig = DeviceModel::configcreate($ip);
if ($createConfig->success === "true") {
$this->layout()->setFlash("Backup wurde erfolgreich erstellt", "success");
} elseif (!TT_MBI_API_ENABLE) {
$this->layout()->setFlash("Backup konnte nicht erstellt werden. Fehler: Schnittstellenserver wurde vom Admin deaktiviert.", "error");
} else {
$this->layout()->setFlash("Backup konnte nicht erstellt werden. Fehler: " . $createConfig->error, "error");
}
$returnUrl = "Device";
$returnAction = "Detail";
$returnVariables['id'] = $id;
$this->redirect($returnUrl, $returnAction, $returnVariables);
}
private function changeoltSplitter($id, $portid, $ports)
{
$changeOltSplitter = DeviceModel::changeoltSplitter($id, $portid, $ports);
echo json_encode($changeOltSplitter);
exit;
}
private function getoltInfo($ip, $portid, $adv)
{
$r = $this->request;
$id = $r->id;
$getOltInfo = DeviceModel::getoltInfo($ip, $portid, $adv);
echo json_encode($getOltInfo);
exit;
}
private function getontInfo($ip, $portid, $ont)
{
$r = $this->request;
$id = $r->id;
$getOntInfo = DeviceModel::getontInfo($ip, $portid, $ont);
echo json_encode($getOntInfo);
exit;
}
private function deviceoltserviceporttimestamp($ip)
{
$deviceoltserviceporttimestamp = DeviceModel::deviceoltserviceporttimestamp($ip);
echo json_encode($deviceoltserviceporttimestamp);
exit;
}
private function deviceoltserviceportrefresh($ip)
{
$deviceoltserviceportrefresh = DeviceModel::deviceoltserviceportrefresh($ip);
echo json_encode($deviceoltserviceportrefresh);
exit;
}
private function getontInfoMac($ip, $portid, $ont)
{
$r = $this->request;
$id = $r->id;
$getOntInfo = DeviceModel::getontInfoMac($ip, $portid, $ont);
echo json_encode($getOntInfo);
exit;
}
private function getDevices()
{
if (!$this->me->is(["Admin"])) {
if ($this->allowedPops === null) return [];
}
$devices = DeviceModel::search(['popIds' => $this->allowedPops]);
foreach ($devices as $device) {
$locationText = "";
$locationUrl = "";
if ($device->pop_id && trim($device->pop->name)) {
$locationText = $device->pop->name;
$locationUrl = self::getUrl("Pop", "Detail", ["id" => $device->pop->id]);
} else if (trim($device->addr_street)) {
$locationText = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
$locationUrl = "http://maps.google.com/?q=" . $locationText;
} else if (trim($device->gps_lat)) {
$locationText = $device->gps_lat . " , " . $device->gps_long;
$locationUrl = "http://maps.google.com/?q=" . $locationText;
}
$backup = 'na';
if ($device->last_config_backup) {
if (time() - $device->last_config_backup <= 172800) {
$backup = 'ok';
if ($device->autobackup == 1) {
$backup = 'auto';
}
} else {
$backup = 'aged';
}
}
$data[] = [
"id" => $device->id,
"name" => $device->name,
"devicetype" => $device->devicetype->name,
"devicemanufactor" => $device->devicetype->devicemanufactor->name,
"locationText" => $locationText,
"locationUrl" => $locationUrl,
"ip" => $device->ip,
"mac" => $device->mac,
"serial" => $device->serial,
"zabbix_online" => $device->zabbix_online,
"zabbix_host_id" => $device->zabbix_host_id,
"price" => $device->price != "0.00" ? $device->price : $device->devicetype->price,
"power" => $device->power != "0.00" ? intval($device->power) : intval($device->devicetype->power),
"backup" => $backup,
];
}
return $data ?? [];
}
protected function zabbixConsolidationAction()
{
if (!$this->me->is(["Admin"])) {
$this->layout()->setFlash("Keine Berechtigung", "error");
$this->redirect("Dashboard");
}
$JSGlobals = [
"BASE_URL" => self::getUrl(""),
"API_URL" => self::getUrl("Device", "api"),
"DEVICE_TYPES" => DevicetypeModel::getAll(),
"POPS" => PopModel::getAll(),
"PAGE_TITLE" => "Zabbix Consolidation",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Devices", "href" => self::getUrl("Device")],
["text" => "Zabbix Consolidation", "href" => self::getUrl("Device", "zabbixConsolidation")]
]
];
$this->layout()->set("vueViewName", "DeviceZabbixConsolidation");
$this->layout()->set("JSGlobals", $JSGlobals);
$this->layout()->setTemplate("VueViews/Vue");
}
protected function getZabbixConsolidationData() {
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
$theToolDevices = DeviceModel::getAll();
$excludedGroupIds = [2, 4, 6, 7];
$allZabbixHosts = $zabbix->getAllHostsWithDetails();
$zabbixHosts = array_filter($allZabbixHosts, function($host) use ($excludedGroupIds) {
$hostGroupIds = array_column($host['hostgroups'] ?? [], 'groupid');
if (empty($hostGroupIds)) {
return true;
}
return empty(array_intersect($hostGroupIds, $excludedGroupIds));
});
$theToolDevicesByName = [];
$theToolDevicesByIp = [];
foreach ($theToolDevices as $device) {
if ($device->ip === '127.0.0.1') continue;
$theToolDevicesByName[$device->name] = $device;
if (!empty($device->ip)) {
$theToolDevicesByIp[$device->ip] = $device;
}
}
$zabbixHostsByVisibleName = [];
$zabbixHostsByIp = [];
foreach ($zabbixHosts as $host) {
if (!empty($host['name'])) {
$zabbixHostsByVisibleName[$host['name']] = $host;
}
if (!empty($host['host'])) {
$zabbixHostsByIp[$host['host']] = $host;
}
}
$results = [
'mismatchedNames' => [],
'inTheToolOnly' => [],
'inZabbixOnly' => [],
'noSnmp' => [],
'noCoordsInZabbix' => [],
'mismatchedCoords' => [],
];
foreach ($theToolDevices as $ttDevice) {
if ($ttDevice->ip === '127.0.0.1') continue;
$deviceDetails = new Device($ttDevice->id);
$theToolData = ['id' => $deviceDetails->id, 'data' => (array) $deviceDetails->data];
if ($deviceDetails->pop && $deviceDetails->pop->id) {
$theToolData['data']['pop'] = (array) $deviceDetails->pop->data;
}
if (!isset($zabbixHostsByVisibleName[$ttDevice->name]) && !isset($zabbixHostsByIp[$ttDevice->ip])) {
$results['inTheToolOnly'][] = $theToolData;
} else {
if (isset($zabbixHostsByVisibleName[$ttDevice->name])) {
$zbxHost = $zabbixHostsByVisibleName[$ttDevice->name];
$ttLat = (float)($theToolData['data']['pop']['gps_lat'] ?? $theToolData['data']['gps_lat'] ?? 0);
$ttLon = (float)($theToolData['data']['pop']['gps_long'] ?? $theToolData['data']['gps_long'] ?? 0);
$zbxLat = (float)($zbxHost['inventory']['location_lat'] ?? 0);
$zbxLon = (float)($zbxHost['inventory']['location_lon'] ?? 0);
if (($ttLat || $ttLon || $zbxLat || $zbxLon) && (abs($ttLat - $zbxLat) > 0.0001 || abs($ttLon - $zbxLon) > 0.0001)) {
$results['mismatchedCoords'][] = ['theTool' => $theToolData, 'zabbix' => $zbxHost];
}
}
}
}
foreach ($zabbixHosts as $zbxHost) {
if (empty($zbxHost['name'])) continue;
if (!isset($theToolDevicesByName[$zbxHost['name']])) {
$results['inZabbixOnly'][] = $zbxHost;
}
$hasSnmp = false;
if (!empty($zbxHost['parentTemplates'])) {
foreach ($zbxHost['parentTemplates'] as $template) {
if (stripos($template['name'], 'icmp ping') === false) {
$hasSnmp = true;
break;
}
}
}
if (!$hasSnmp) {
$results['noSnmp'][] = $zbxHost;
}
$zbxLat = (float)($zbxHost['inventory']['location_lat'] ?? 0);
$zbxLon = (float)($zbxHost['inventory']['location_lon'] ?? 0);
$theToolDeviceForCoords = $theToolDevicesByName[$zbxHost['name']] ?? null;
$theToolDataForCoords = null;
if($theToolDeviceForCoords){
$deviceDetails = new Device($theToolDeviceForCoords->id);
$theToolDataForCoords = ['id' => $deviceDetails->id, 'data' => (array) $deviceDetails->data];
if ($deviceDetails->pop && $deviceDetails->pop->id) {
$theToolDataForCoords['data']['pop'] = (array) $deviceDetails->pop->data;
}
}
if ($zbxLat == 0 && $zbxLon == 0) {
$results['noCoordsInZabbix'][] = [
'zabbix' => $zbxHost,
'theTool' => $theToolDataForCoords
];
}
}
foreach ($zabbixHosts as $zbxHost) {
if(isset($theToolDevicesByIp[$zbxHost['host']])) {
$ttDeviceFromIp = new Device($theToolDevicesByIp[$zbxHost['host']]->id);
$theToolData = ['id' => $ttDeviceFromIp->id, 'data' => $ttDeviceFromIp->data];
if ($ttDeviceFromIp->pop && $ttDeviceFromIp->pop->id) {
$theToolData['data']['pop'] = $ttDeviceFromIp->pop->data;
}
if($ttDeviceFromIp->name !== $zbxHost['name']){
$results['mismatchedNames'][] = ['theTool' => $theToolData, 'zabbix' => $zbxHost];
}
}
}
self::returnJson($results);
}
protected function getZabbixTemplates() {
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
$templateNames = [
"ICMP Ping",
"1_Default XINON Template",
"1_XINON Extreme EXOS by SNMP",
"1_MIKROTIK_TEMPLATE"
];
$templates = $zabbix->getTemplatesByNames($templateNames);
$formattedTemplates = array_map(function($template) {
return ['value' => $template['templateid'], 'text' => $template['name']];
}, $templates);
self::returnJson($formattedTemplates);
}
protected function createZabbixHost() {
$this->postData = json_decode(file_get_contents('php://input'), true);
$deviceId = $this->postData['deviceId'] ?? null;
$templateIds = $this->postData['templateIds'] ?? null;
if (!$deviceId || !$templateIds) {
self::sendError("Device ID or Template ID is missing.");
}
$device = DeviceModel::getOne($deviceId);
if (!$device || !$device->id) {
self::sendError("Device not found in TheTool.");
}
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
$groupName = "Discovered hosts";
$existingHosts = $zabbix->getHosts($device->name, $device->ip);
if (!empty($existingHosts)) {
self::sendError("Host with this name or IP already exists in Zabbix.");
}
$groupId = $zabbix->getHostGroupIdByName($groupName);
if (!$groupId) {
self::sendError("Host group '$groupName' not found in Zabbix.");
}
$result = $zabbix->createHost($device->name, $device->ip, $groupId, $templateIds);
if (isset($result['hostids'])) {
$device->zabbix_host_id = $result['hostids'][0];
$device->save();
self::returnJson(['success' => true, 'message' => 'Host successfully created in Zabbix and linked in TheTool.']);
} else {
self::sendError("Failed to create host in Zabbix: " . json_encode($result['error'] ?? 'Unknown error'));
}
}
protected function updateTheToolCoordinatesAction() {
$deviceId = $this->postData['deviceId'] ?? null;
$lat = $this->postData['lat'] ?? null;
$lon = $this->postData['lon'] ?? null;
if (!$deviceId || $lat === null || $lon === null) {
self::sendError("Device ID or coordinates are missing.");
}
$device = new Device($deviceId);
if (!$device->id) {
self::sendError("Device not found.");
}
if ($device->pop_id) {
self::sendError("Koordinaten können nicht geändert werden, da sie vom POP-Standort geerbt werden.");
}
$device->update([
'gps_lat' => $lat,
'gps_long' => $lon
]);
$device->save();
self::returnJson(['success' => true, 'message' => 'TheTool coordinates updated successfully.']);
}
protected function updateZabbixCoordinates() {
$this->postData = json_decode(file_get_contents('php://input'), true);
$hostId = $this->postData['hostId'] ?? null;
$lat = $this->postData['lat'] ?? null;
$lon = $this->postData['lon'] ?? null;
if (!$hostId || $lat === null || $lon === null) {
self::sendError("Host ID or coordinates are missing.");
}
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
$inventoryData = [
'location_lat' => (string)$lat,
'location_lon' => (string)$lon,
];
$result = $zabbix->updateHostInventory($hostId, $inventoryData);
if (isset($result['hostids'])) {
self::returnJson(['success' => true, 'message' => 'Coordinates updated successfully in Zabbix.']);
} else {
self::sendError("Failed to update coordinates in Zabbix: " . json_encode($result['error'] ?? 'Unknown error'));
}
}
}