src/Controller/Api/SensorController.php line 232

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api;
  3. // use OpenApi\Annotations as OA;
  4. use App\Entity\Gpio;
  5. use App\Entity\Sensor;
  6. use App\Entity\SmsLog;
  7. use App\Entity\Trade;
  8. use App\Repository\SmsLogRepository;
  9. use App\Service\ObserverDeployManifest;
  10. use App\Service\SensorRemoteService;
  11. use App\Service\SmsLogger;
  12. use App\Service\WhereEverSmsService;
  13. use FOS\RestBundle\Controller\AbstractFOSRestController;
  14. use FOS\RestBundle\Controller\Annotations as Rest;
  15. use Psr\Log\LoggerInterface;
  16. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  17. use OpenApi\Annotations as OA;
  18. use Symfony\Component\HttpClient\HttpClient;
  19. use Symfony\Component\HttpFoundation\JsonResponse;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\HttpFoundation\Response;
  22. use Symfony\Component\HttpKernel\KernelInterface;
  23. use Symfony\Component\Mercure\HubInterface;
  24. use Symfony\Component\Mercure\Update;
  25. use Symfony\Component\Serializer\Encoder\JsonEncoder;
  26. use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
  27. use Symfony\Component\Serializer\Serializer;
  28. class SensorController extends AbstractFOSRestController
  29. {
  30.     protected $logger;
  31.     protected $serializer;
  32.     private $appKernel;
  33.     private $sensorRemoteService;
  34.     private HubInterface $hub;
  35.     public function __construct(LoggerInterface $loggerKernelInterface $appKernelSensorRemoteService $sensorRemoteServiceHubInterface $hub)
  36.     {
  37.         $encoders = [new JsonEncoder()];
  38.         $normalizers = [new ObjectNormalizer()];
  39.         $this->logger $logger;
  40.         $this->appKernel $appKernel;
  41.         $this->serializer = new Serializer($normalizers$encoders);
  42.         $this->sensorRemoteService $sensorRemoteService;
  43.         $this->hub $hub;
  44.     }
  45.     public function index(LoggerInterface $logger)
  46.     {
  47.         return $this->render('api/index.html.twig', [
  48.             'controller_name' => 'ApiController',
  49.         ]);
  50.     }
  51.     /**
  52.      * @Route("/sensor/register", name="sensor_register", methods={"POST"})
  53.      *
  54.      * @OA\Post(
  55.      *     summary="Register new sensor",
  56.      *
  57.      *
  58.      *     @OA\Parameter(
  59.      *         name="body",
  60.      *         description="Post data.",
  61.      *         in="body",
  62.      *         required=true,
  63.      *
  64.      *         @OA\Schema(
  65.      *             type="string",
  66.      *             required={"serial", "createdAt"},
  67.      *
  68.      *             @OA\Property(
  69.      *                 property="serial",
  70.      *                 type="string",
  71.      *                 minLength=1,
  72.      *                 example="10000000b230f68d"
  73.      *             ),
  74.      *             @OA\Property(
  75.      *                 property="createdAt",
  76.      *                 type="datetime",
  77.      *                 minLength=1,
  78.      *                 example="2020-05-10 12:13:01"
  79.      *             )
  80.      *         )
  81.      *     ),
  82.      *
  83.      *     @OA\Response(
  84.      *         response=200,
  85.      *         description="Returns status 200 and the modified contact.",
  86.      *
  87.      *         @OA\Schema(
  88.      *             type="object",
  89.      *             properties={
  90.      *
  91.      *                 @OA\Property(property="id", type="integer"),
  92.      *             }
  93.      *         )
  94.      *     ),
  95.      *
  96.      *     @OA\Response(
  97.      *         response=404,
  98.      *         description="Returns status 404 if there is no contact with the given id."
  99.      *     )
  100.      * )
  101.      *
  102.      * @return View
  103.      */
  104.     public function register(Request $request)
  105.     {
  106.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  107.         $em $this->getDoctrine()->getManager();
  108.         $parameters json_decode($request->getContent(), true);
  109.         $serial $parameters['serial'];
  110.         $createdDate $parameters['createdAt'];
  111.         $trade $this->getDoctrine()->getRepository(Trade::class)->find($parameters['tradeId']);
  112.         if ($sensor $repository->findOneBySerial($serial)) {
  113.             // check if sensor has already a port
  114.             if (!$sensor->getPort()) {
  115.                 $port $repository->getNextFreePort($trade);
  116.                 $sensor->setPort($port);
  117.                 $em->persist($sensor);
  118.                 $em->flush();
  119.                 //  sensor is already but has a new port so we need to give an state ok back
  120.                 $objContent = [
  121.                     'id' => $sensor->getId(),
  122.                     'serial' => $sensor->getSerial(),
  123.                     'createdAt' => $sensor->getCreatedAt()->format('Y-m-d H:i:s'),
  124.                     'port' => $sensor->getPort(),
  125.                 ];
  126.                 $jsonContent $this->serializer->serialize($objContent'json');
  127.                 $response = new Response($jsonContent);
  128.                 $response->headers->set('Content-Type''application/json');
  129.                 $response->setStatusCode(Response::HTTP_CREATED);
  130.                 return $response;
  131.             }
  132.             //  sensor is already registered
  133.             $objContent = [
  134.                 'id' => $sensor->getId(),
  135.                 'serial' => $sensor->getSerial(),
  136.                 'createdAt' => $sensor->getCreatedAt()->format('Y-m-d H:i:s'),
  137.                 'port' => $sensor->getPort(),
  138.             ];
  139.             $jsonContent $this->serializer->serialize($objContent'json');
  140.             $response = new Response($jsonContent);
  141.             // / $response = new Response();
  142.             $response->headers->set('Content-Type''application/json');
  143.             $response->setStatusCode(Response::HTTP_CONFLICT);
  144.             // $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  145.             return $response;
  146.         }
  147.         $sensor = new Sensor();
  148.         $sensor->setSerial($serial);
  149.         $sensor->setCreatedAt(new \DateTime());
  150.         $sensor->setLastOnline(new \DateTime());
  151.         $sensor->setTrade($trade);
  152.         $sensor->setObserverState(false);
  153.         $sensor->setInstalled(0);
  154.         $sensor->setState(Sensor::STATE_ONLINE);
  155.         $port $repository->getNextFreePort($trade);
  156.         $sensor->setPort($port);
  157.         $em->persist($sensor);
  158.         $em->flush();
  159.         $objContent = [
  160.             'id' => $sensor->getId(),
  161.             'serial' => $sensor->getSerial(),
  162.             'createdAt' => $sensor->getCreatedAt()->format('Y-m-d H:i:s'),
  163.             'port' => $sensor->getPort(),
  164.         ];
  165.         $jsonContent $this->serializer->serialize($objContent'json');
  166.         $response = new Response($jsonContent);
  167.         $response->headers->set('Content-Type''application/json');
  168.         $response->setStatusCode(Response::HTTP_CREATED);
  169.         return $response;
  170.     }
  171.     /**
  172.      * @Route("/sensor/lastOnline", name="sensor_lastonline", methods={"PUT"})
  173.      *
  174.      * @OA\Post(
  175.      *     summary="Set last online datetime for sensor.",
  176.      *
  177.      *
  178.      *     @OA\Parameter(
  179.      *         name="body",
  180.      *         description="Post data.",
  181.      *         in="body",
  182.      *         required=true,
  183.      *
  184.      *         @OA\Schema(type="string")
  185.      *     ),
  186.      *
  187.      *     @OA\Response(
  188.      *         response=200,
  189.      *         description="Returns status 200 and the modified contact.",
  190.      *
  191.      *         @OA\Schema(
  192.      *             type="object",
  193.      *             properties={
  194.      *
  195.      *                 @OA\Property(property="id", type="integer"),
  196.      *             }
  197.      *         )
  198.      *     ),
  199.      *
  200.      *     @OA\Response(
  201.      *         response=404,
  202.      *         description="Returns status 404 if there is no contact with the given id."
  203.      *     )
  204.      * )
  205.      */
  206.     public function lastOnlineAction(Request $request): JsonResponse
  207.     {
  208.         $responseArray = ['ok' => 'ok'];
  209.         // return new JsonResponse($responseArray);
  210.         $requestBody $request->getContent();
  211.         $data json_decode($requestBody);
  212.         $em $this->getDoctrine()->getManager();
  213.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial]);
  214.         if ($sensor) {
  215.             // $this->logger->error('SensorController API: lastOnlineAction: '.$sensor->getId());
  216.             $lastOnlineDateTime = new \DateTime();
  217.             $sensor->setLastOnline($lastOnlineDateTime);
  218.             $sensor->setState(1);
  219.             $em->persist($sensor);
  220.             $em->flush();
  221.             $command $sensor->getPendingCommand();
  222.             if ($command) {
  223.                 $sensor->setPendingCommand(null);
  224.                 $em->flush();
  225.             }
  226.             $responseArray = ['ok' => 'ok''command' => $command];
  227.         } else {
  228.             // $this->logger->error('SensorController API: lastOnlineAction: ERROR: '.$data->serial);
  229.             $responseArray = ['error' => 'Sensor '.$data->serial.' not found'];
  230.         }
  231.         return new JsonResponse($responseArray);
  232.     }
  233.     /**
  234.      * @Route("/sensor/keepAlive", name="sensor_keep_alive", methods={"GET"})
  235.      *
  236.      * KeepAlive-Ping vom Sensor alle 5 Minuten.
  237.      * Aufruf: GET /sensor/keepAlive?serial=SENSOR_SERIAL
  238.      *
  239.      * Ergänzt lastOnlineAction (PUT + JSON-Body): NB-IoT-Sensoren unterstützen
  240.      * je nach Firmware-Stand keinen HTTP-PUT mit JSON-Body. Ein einfacher GET-Request
  241.      * mit Query-Parameter ist auf allen Modulen (z.B. SIM7000, BC66) per AT-Befehl
  242.      * ohne weitere Konfiguration realisierbar.
  243.      */
  244.     public function keepAliveAction(Request $request): JsonResponse
  245.     {
  246.         $serial $request->query->get('serial');
  247.         if (!$serial) {
  248.             return new JsonResponse(['error' => 'serial fehlt'], Response::HTTP_BAD_REQUEST);
  249.         }
  250.         $em     $this->getDoctrine()->getManager();
  251.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $serial]);
  252.         if (!$sensor) {
  253.             return new JsonResponse(['error' => 'Sensor nicht gefunden: '.$serial], Response::HTTP_NOT_FOUND);
  254.         }
  255.         $now = new \DateTime();
  256.         $sensor->setLastOnline($now);
  257.         $sensor->setState(1);
  258.         $em->flush();
  259.         $this->logger->info('KeepAlive: Sensor '.$serial.' ('.$sensor->getId().')', [
  260.             'sensor_id' => $sensor->getId(),
  261.             'ts'        => $now->format('Y-m-d H:i:s'),
  262.         ]);
  263.         try {
  264.             $this->hub->publish(new Update(
  265.                 'https://acobesmart.com/mqtt_monitor',
  266.                 json_encode([
  267.                     'type'          => 'heartbeat',
  268.                     'source'        => 'http',
  269.                     'sensor_serial' => $serial,
  270.                     'sensor_id'     => $sensor->getId(),
  271.                     'time'          => $now->format('H:i:s'),
  272.                 ], JSON_THROW_ON_ERROR)
  273.             ));
  274.         } catch (\Throwable $e) {
  275.             // Mercure nicht erreichbar â€” Request läuft weiter
  276.         }
  277.         return new JsonResponse(['ok' => 'ok''ts' => $now->format('Y-m-d H:i:s')]);
  278.     }
  279.     /**
  280.      * @Route("/sensor/observerLastOnline", name="observer_lastonline", methods={"PUT"})
  281.      *
  282.      * @OA\Put(
  283.      *     summary="Set last online datetime for sensor.",
  284.      *
  285.      *
  286.      *     @OA\Parameter(
  287.      *         name="body",
  288.      *         description="Post data.",
  289.      *         in="body",
  290.      *         required=true,
  291.      *
  292.      *         @OA\Schema(
  293.      *             type="string",
  294.      *             required={"sensorId"},
  295.      *
  296.      *             @OA\Property(
  297.      *                 property="sensorId",
  298.      *                 type="string",
  299.      *                 minLength=1,
  300.      *                 example=1
  301.      *             ),
  302.      *         )
  303.      *     ),
  304.      *
  305.      *     @OA\Response(
  306.      *         response=200,
  307.      *         description="Returns status 200 and the modified contact.",
  308.      *
  309.      *         @OA\Schema(
  310.      *             type="object",
  311.      *             properties={
  312.      *
  313.      *                 @OA\Property(property="id", type="integer"),
  314.      *             }
  315.      *         )
  316.      *     ),
  317.      *
  318.      *     @OA\Response(
  319.      *         response=404,
  320.      *         description="Returns status 404 if there is no contact with the given id."
  321.      *     )
  322.      * )
  323.      */
  324.     public function observerLastOnlineAction(Request $request): JsonResponse
  325.     {
  326.         $requestBody $request->getContent();
  327.         $data json_decode($requestBody);
  328.         $em $this->getDoctrine()->getManager();
  329.         $sensor false;
  330.         if ($data->sensorId) {
  331.             $sensor $em->getRepository(Sensor::class)->find($data->sensorId);
  332.         }
  333.         if ($sensor) {
  334.             $lastOnlineDate = new \DateTime();
  335.             $sensor->setObserverLastOnline($lastOnlineDate);
  336.             // $sensor->setObserverState(1);
  337.             $em->persist($sensor);
  338.             $em->flush();
  339.             $responseArray = ['status' => 'ok'];
  340.         } else {
  341.             $responseArray = ['error' => 'Sensor '.$data->sensorId.' not found'];
  342.         }
  343.         return new JsonResponse($responseArray);
  344.     }
  345.     /**
  346.      * @Route("/sensor/getGpios", name="sensor_get_gpios", methods={"GET"})
  347.      *
  348.      * @OA\Get(
  349.      *     summary="Get registered Gpios",
  350.      *
  351.      *
  352.      *     @OA\Parameter(
  353.      *         name="body",
  354.      *         description="Post data.",
  355.      *         in="body",
  356.      *         required=true,
  357.      *
  358.      *         @OA\Schema(
  359.      *             type="string",
  360.      *             required={"sensorId"},
  361.      *
  362.      *             @OA\Property(
  363.      *                 property="sensorId",
  364.      *                 type="string",
  365.      *                 minLength=1,
  366.      *                 example=1
  367.      *             ),
  368.      *         )
  369.      *     ),
  370.      *
  371.      *     @OA\Response(
  372.      *         response=200,
  373.      *         description="Returns status 200 and the modified contact.",
  374.      *
  375.      *         @OA\Schema(
  376.      *             type="object",
  377.      *             properties={
  378.      *
  379.      *                 @OA\Property(property="id", type="integer"),
  380.      *             }
  381.      *         )
  382.      *     ),
  383.      *
  384.      *     @OA\Response(
  385.      *         response=404,
  386.      *         description="Returns status 404 if there is no contact with the given id."
  387.      *     )
  388.      * )
  389.      */
  390.     public function getGpiosAction(Request $request)
  391.     {
  392.         $this->logger->debug('SensorController API: getGpiosAction');
  393.         $requestBody $request->getContent();
  394.         $data json_decode($requestBody);
  395.         $em $this->getDoctrine()->getManager();
  396.         $sensor false;
  397.         // echo "HIER: ".$data->serial; exit();
  398.         if ($data->serial) {
  399.             $sensor $em->getRepository(Sensor::class)->findOneBySerial($data->serial);
  400.         }
  401.         $gpios = [];
  402.         $responseArray = [];
  403.         if ($sensor) {
  404.             $this->logger->debug('SensorController API: sensor found');
  405.             if (true == $sensor->getInstalled()) {
  406.                 $gpios $sensor->getGpios();
  407.                 foreach ($gpios as $gpio) {
  408.                     if (3000 != $gpio->getNumber()) {
  409.                         $responseArray[(int) $gpio->getNumber()] = $gpio->getTypeReadable();
  410.                     }
  411.                 }
  412.             } else {
  413.                 switch ($sensor->getTrade()->getName()) {
  414.                     case 'Aufzug':
  415.                         // sensor not installed so give first all gpios back for installation
  416.                         $responseArray[Gpio::GPIO_TYPE_POWER] = 'Netz';
  417.                         $responseArray[Gpio::GPIO_TYPE_SECURITY_CIRCUIT_FRONT] = 'Sicherheitskreis vor den Türen';
  418.                         $responseArray[Gpio::GPIO_TYPE_SECURITY_CIRCUIT_BACK] = 'Sicherheitskreis nach den Türen';
  419.                         $responseArray[Gpio::GPIO_TYPE_TRIP_UP] = 'Fahrt auf';
  420.                         $responseArray[Gpio::GPIO_TYPE_TRIP_DOWN] = 'Fahrt ab';
  421.                         $responseArray[Gpio::GPIO_TYPE_SECURITY_CIRCUIT_HOISTWAY_DOOR] = 'Sicherheitskreis Schachttüren';
  422.                         // $responseArray[Gpio::GPIO_TYPE_SECURITY_CIRCUIT_HOISTWAY_DOOR] = 'Reserve';
  423.                         $responseArray[Gpio::GPIO_TYPE_FLUSHNESS] = 'Bündig';
  424.                         $responseArray[Gpio::GPIO_TYPE_CABINLIGHT] = 'Kabinenbeleuchtung';
  425.                         $responseArray[Gpio::GPIO_TYPE_COLLECTIVE_DISORDER] = 'Sammelstörung';
  426.                         $responseArray[Gpio::GPIO_TYPE_SMOKE_EXTRACTION_COLLECTIVE_DISORDER] = 'Tür 1';
  427.                         $responseArray[Gpio::GPIO_TYPE_WATERLEVEL_WARNING] = 'Wasserstandsmelder';
  428.                         $responseArray[Gpio::GPIO_TYPE_DOOR_TWO_CLOSE] = 'Tür 2';
  429.                         $responseArray[Gpio::GPIO_TYPE_DOOR_TWO_OPEN] = 'Tür 2    auf';
  430.                         break;
  431.                     case 'Nea':
  432.                         break;
  433.                     case 'Pumpen':
  434.                         $responseArray[17] = '17';
  435.                         $responseArray[4] = '4';
  436.                         $responseArray[14] = '14';
  437.                         $responseArray[15] = '15';
  438.                         $responseArray[18] = '18';
  439.                         $responseArray[23] = '23';
  440.                         $responseArray[24] = '24';
  441.                         $responseArray[25] = '25';
  442.                         $responseArray[8] = '8';
  443.                         $responseArray[7] = '7';
  444.                         $responseArray[27] = '27';
  445.                         $responseArray[22] = '22';
  446.                         $responseArray[12] = '12';
  447.                         $responseArray[16] = '16';
  448.                         // $responseArray[20] = '20';
  449.                         // $responseArray[21] = '21';
  450.                         $responseArray[26] = '26';
  451.                         $responseArray[19] = '19';
  452.                         $responseArray[13] = '13';
  453.                         $responseArray[6] = '6';
  454.                         break;
  455.                 }
  456.             }
  457.             $jsonContent $this->serializer->serialize($responseArray'json');
  458.             $response = new Response($jsonContent);
  459.             $response->headers->set('Content-Type''application/json');
  460.             // $response->setStatusCode(Response::HTTP_CREATED);
  461.             return $response;
  462.         }
  463.         $this->logger->debug('SensorController API: sensor NOT found');
  464.         $responseArray = ['error' => 'Sensor '.$data->serial.' not found'];
  465.         return new JsonResponse($responseArray);
  466.     }
  467.     /**
  468.      * @Route("/sensor/sshkey", name="sensor_ssh_key", methods={"PUT"})
  469.      *
  470.      * Nimmt den vom Pi gemeldeten oeffentlichen SSH-Schluessel entgegen (Observer >= 1.5.7,
  471.      * ensureSshKey in heartbeat.py) und speichert ihn NUR am Sensor-Datensatz - es wird
  472.      * nirgends automatisch etwas freigeschaltet (Freischaltung: app:sensor-authorize-keys
  473.      * + manuelles Anhaengen an authorized_keys). Strenge Format-Pruefung, da der Endpoint
  474.      * wie alle Sensor-Endpoints unauthentifiziert ist.
  475.      */
  476.     public function sshKeyAction(Request $request): JsonResponse
  477.     {
  478.         $data json_decode($request->getContent());
  479.         if (null === $data || empty($data->serial) || empty($data->pubkey)) {
  480.             return new JsonResponse(['error' => 'serial and pubkey required'], Response::HTTP_BAD_REQUEST);
  481.         }
  482.         $pubkey trim((string) $data->pubkey);
  483.         if (strlen($pubkey) > 1000
  484.             || !preg_match('/^(ssh-rsa|ssh-ed25519) AAAA[A-Za-z0-9+\/=]+( [^\s]{1,64})?$/'$pubkey)) {
  485.             return new JsonResponse(['error' => 'invalid pubkey format'], Response::HTTP_BAD_REQUEST);
  486.         }
  487.         $keyStatus = isset($data->keyStatus) ? substr(trim((string) $data->keyStatus), 032) : null;
  488.         $em $this->getDoctrine()->getManager();
  489.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial]);
  490.         if (!$sensor) {
  491.             return new JsonResponse(['error' => 'Sensor not found'], Response::HTTP_NOT_FOUND);
  492.         }
  493.         $sensor->setSshPublicKey($pubkey);
  494.         $sensor->setSshKeyStatus($keyStatus);
  495.         $sensor->setSshKeyReportedAt(new \DateTime());
  496.         $em->persist($sensor);
  497.         $em->flush();
  498.         return new JsonResponse(['status' => 'ok']);
  499.     }
  500.     /**
  501.      * @Route("/sensor/observerVersion", name="observer_version", methods={"PUT"})
  502.      *
  503.      * @OA\Put(
  504.      *     summary="Set version of script running at sensor.",
  505.      *
  506.      *
  507.      *     @OA\Parameter(
  508.      *         name="body",
  509.      *         description="Post data.",
  510.      *         in="body",
  511.      *         required=true,
  512.      *
  513.      *         @OA\Schema(
  514.      *             type="string",
  515.      *             required={"serial", "version"},
  516.      *
  517.      *             @OA\Property(
  518.      *                 property="serial",
  519.      *                 type="string",
  520.      *                 minLength=1,
  521.      *                 example=1
  522.      *             ),
  523.      *             @OA\Property(
  524.      *                 property="version",
  525.      *                 type="string",
  526.      *                 minLength=1,
  527.      *                 example=1
  528.      *             ),
  529.      *         )
  530.      *     ),
  531.      *
  532.      *     @OA\Response(
  533.      *         response=200,
  534.      *         description="Returns status 200 and the modified contact.",
  535.      *
  536.      *         @OA\Schema(
  537.      *             type="object",
  538.      *             properties={
  539.      *
  540.      *                 @OA\Property(property="id", type="integer"),
  541.      *             }
  542.      *         )
  543.      *     ),
  544.      *
  545.      *     @OA\Response(
  546.      *         response=404,
  547.      *         description="Returns status 404 if there is no contact with the given id."
  548.      *     )
  549.      * )
  550.      */
  551.     public function observerVersionAction(Request $request): JsonResponse
  552.     {
  553.         $requestBody $request->getContent();
  554.         $data json_decode($requestBody);
  555.         $em $this->getDoctrine()->getManager();
  556.         $sensorRepo $em->getRepository(Sensor::class);
  557.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial]);
  558.         // Manche Observer melden die Version mit angehängtem Zeilenumbruch aus der .version-Datei
  559.         $version trim($data->version);
  560.         if ($sensor) {
  561.             $sensor->setObserverVersion($version);
  562.             $em->persist($sensor);
  563.             $em->flush();
  564.             $responseArray = ['status' => 'ok'];
  565.         } else {
  566.             $responseArray = ['error' => 'Sensor '.$data->serial.' not found'];
  567.         }
  568.         return new JsonResponse($responseArray);
  569.     }
  570.     /**
  571.      * @Route("/sensor/getSensorId", name="get_sensor_id", methods={"GET"})
  572.      *
  573.      * @OA\Get(
  574.      *     summary="Get internal ID of sensor",
  575.      *
  576.      *
  577.      *     @OA\Parameter(
  578.      *         name="body",
  579.      *         description="Post data.",
  580.      *         in="body",
  581.      *         required=true,
  582.      *
  583.      *         @OA\Schema(
  584.      *             type="string",
  585.      *             required={"serial"},
  586.      *
  587.      *             @OA\Property(
  588.      *                 property="serial",
  589.      *                 type="string",
  590.      *                 minLength=1,
  591.      *                 example=1
  592.      *             ),
  593.      *         )
  594.      *     ),
  595.      *
  596.      *     @OA\Response(
  597.      *         response=200,
  598.      *         description="Returns status 200 and the modified contact.",
  599.      *
  600.      *         @OA\Schema(
  601.      *             type="object",
  602.      *             properties={
  603.      *
  604.      *                 @OA\Property(property="id", type="integer"),
  605.      *             }
  606.      *         )
  607.      *     ),
  608.      *
  609.      *     @OA\Response(
  610.      *         response=404,
  611.      *         description="Returns status 404 if there is no contact with the given id."
  612.      *     )
  613.      * )
  614.      */
  615.     public function getSensorIdAction(Request $request): JsonResponse
  616.     {
  617.         $requestBody $request->getContent();
  618.         $data json_decode($requestBody);
  619.         $em $this->getDoctrine()->getManager();
  620.         $sensorRepo $em->getRepository(Sensor::class);
  621.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial]);
  622.         if ($sensor) {
  623.             $responseArray = ['id' => $sensor->getId()];
  624.         } else {
  625.             $responseArray = ['error' => 'Sensor '.$data->serial.' not found'];
  626.         }
  627.         return new JsonResponse($responseArray);
  628.     }
  629.     /**
  630.      * @Route("/sensor/sendSMS/{action}/{sensor}", name="sensor_send_sms", methods={"POST"})
  631.      *
  632.      * @param mixed $action
  633.      * @param mixed $sensor
  634.      */
  635.     public function sendSMS($actionSensor $sensor)
  636.     {
  637.         if ($sensor->getPhone()) {
  638.             $client HttpClient::create();
  639.             switch ($action) {
  640.                 case 'test':
  641.                     $query['action'] = 'Test';
  642.                     break;
  643.                 case 'reboot':
  644.                     $query['action'] = 'Reboot';
  645.                     break;
  646.                 case 'restartObserver':
  647.                     $query['action'] = 'Restart';
  648.                     break;
  649.                 case 'reconnect':
  650.                     $query['action'] = 'Reconnect';
  651.                     break;
  652.             }
  653.             $query = ['phone' => $sensor->getPhone(), 'action' => $query['action']];
  654.             $response $client->request('POST''http://acosms.duckdns.org:8878/index.php', [
  655.                 // these values are automatically encoded before including them in the URL
  656.                 'body' => $query,
  657.             ]);
  658.             if ('OK' == trim($response->getContent())) {
  659.                 $response = ['state' => 'OK''msg' => 'OK'];
  660.             } else {
  661.                 $response = ['state' => 'error''msg' => trim($response->getContent())];
  662.             }
  663.         } else {
  664.             $response = ['state' => 'error''msg' => 'No phone number given!'];
  665.         }
  666.         return new JsonResponse($response);
  667.     }
  668.     /** Sicherheits-Deckel gegen Massenversand im Batch. */
  669.     private const SMS_BATCH_MAX 100;
  670.     /**
  671.      * Reboot-SMS Ã¼ber WhereEver/Jasper an die ICCID des Sensors (günstiger Weg als normale SMS),
  672.      * Abfrage des Zustellstatus (getSmsDetail) und Protokollierung im SMS-Log.
  673.      *
  674.      * @Route("/sensor/rebootSmsWherever/{sensor}", name="sensor_reboot_sms_wherever", methods={"POST"})
  675.      */
  676.     public function rebootSmsWherever(Sensor $sensorWhereEverSmsService $smsSmsLogger $smsLogger): JsonResponse
  677.     {
  678.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  679.         return new JsonResponse($this->doCommandSms($sensor'Reboot'$sms$smsLogger));
  680.     }
  681.     /**
  682.      * Steuer-SMS mit frei wählbarem Kommando. Sinnvoll, weil ein Reboot nur bei flüchtigen
  683.      * Störungen hilft: Läuft der Pi noch, sind aber die Observer-Dienste gestorben oder hängt
  684.      * die Einwahl, wirken "Restart" bzw. "Reconnect" gezielter. Zulässige Werte stehen in
  685.      * WhereEverSmsService::COMMANDS; alles andere wird abgewiesen, bevor Kosten entstehen.
  686.      *
  687.      * @Route("/sensor/commandSmsWherever/{sensor}/{command}", name="sensor_command_sms_wherever", methods={"POST"})
  688.      */
  689.     public function commandSmsWherever(Sensor $sensorstring $commandWhereEverSmsService $smsSmsLogger $smsLogger): JsonResponse
  690.     {
  691.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  692.         return new JsonResponse($this->doCommandSms($sensor$command$sms$smsLogger));
  693.     }
  694.     /**
  695.      * Batch: Reboot-SMS an alle ausgewählten Sensoren (per Checkbox aus der Sensor-Liste).
  696.      *
  697.      * @Route("/sensor/rebootSmsBatch", name="sensor_reboot_sms_batch", methods={"POST"})
  698.      */
  699.     public function rebootSmsBatch(Request $requestWhereEverSmsService $smsSmsLogger $smsLogger): JsonResponse
  700.     {
  701.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  702.         $payload json_decode($request->getContent(), true);
  703.         $ids = \is_array($payload['ids'] ?? null) ? $payload['ids'] : [];
  704.         $ids array_values(array_unique(array_filter(array_map('intval'$ids))));
  705.         // Ohne Angabe bleibt es beim bisherigen Verhalten (Reboot)
  706.         $command = (string) ($payload['command'] ?? 'Reboot');
  707.         if (empty($ids)) {
  708.             return new JsonResponse(['state' => 'error''msg' => 'Keine Sensoren ausgewählt.']);
  709.         }
  710.         if (!isset(WhereEverSmsService::COMMANDS[$command])) {
  711.             return new JsonResponse(['state' => 'error''msg' => 'Unbekanntes Kommando: '.$command]);
  712.         }
  713.         if (\count($ids) > self::SMS_BATCH_MAX) {
  714.             return new JsonResponse(['state' => 'error''msg' => 'Zu viele Sensoren ausgewählt (max. '.self::SMS_BATCH_MAX.').']);
  715.         }
  716.         $repo $this->getDoctrine()->getRepository(Sensor::class);
  717.         $sent 0;
  718.         $failed 0;
  719.         $results = [];
  720.         foreach ($ids as $id) {
  721.             $sensor $repo->find($id);
  722.             if (null === $sensor) {
  723.                 ++$failed;
  724.                 $results[] = ['id' => $id'ok' => false'error' => 'Sensor nicht gefunden'];
  725.                 continue;
  726.             }
  727.             $r $this->doCommandSms($sensor$command$sms$smsLogger);
  728.             $ok 'OK' === $r['state'];
  729.             $ok ? ++$sent : ++$failed;
  730.             $results[] = [
  731.                 'id' => $id'ok' => $ok'status' => $r['status'] ?? null,
  732.                 'smsLogId' => $r['smsLogId'] ?? null'smsMessageId' => $r['smsMessageId'] ?? null,
  733.                 'error' => $ok null : ($r['msg'] ?? null),
  734.             ];
  735.         }
  736.         return new JsonResponse([
  737.             'state' => 'OK',
  738.             'msg' => sprintf('%d gesendet, %d fehlgeschlagen'$sent$failed),
  739.             'sent' => $sent'failed' => $failed'results' => $results,
  740.         ]);
  741.     }
  742.     /**
  743.      * Sendet die Steuer-SMS, ermittelt den Sofort-Status und schreibt einen SMS-Log-Eintrag.
  744.      * Gemeinsame Logik von Einzel- und Batch-Versand. Das Kommando landet unverändert im
  745.      * Log (Feld messageText), damit später nachvollziehbar ist, was geschickt wurde.
  746.      *
  747.      * @return array{state: string, msg: string, smsMessageId: mixed, status: string|null, smsLogId: int|null}
  748.      */
  749.     private function doCommandSms(Sensor $sensorstring $commandWhereEverSmsService $smsSmsLogger $smsLogger): array
  750.     {
  751.         $sentBy $this->getUser() ? (string) $this->getUser()->getUsername() : null;
  752.         $send $sms->sendCommand($sensor$command);
  753.         if (!$send['ok']) {
  754.             $log $smsLogger->log($sensor$commandnull'Fehler'$sentBy);
  755.             return ['state' => 'error''msg' => $send['error'] ?? 'Versand fehlgeschlagen''smsMessageId' => null'status' => null'smsLogId' => null !== $log $log->getId() : null];
  756.         }
  757.         $status null;
  758.         if (null !== $send['smsMessageId']) {
  759.             $detail $sms->getSmsDetail($send['smsMessageId']);
  760.             $status $detail['ok'] ? $detail['status'] : null;
  761.         }
  762.         $log $smsLogger->log($sensor$commandnull !== $send['smsMessageId'] ? (string) $send['smsMessageId'] : null$status$sentBy);
  763.         return [
  764.             'state' => 'OK',
  765.             'msg' => $command.'-SMS gesendet'.(null !== $status ' (Status: '.$status.')' ''),
  766.             'smsMessageId' => $send['smsMessageId'],
  767.             'status' => $status,
  768.             'smsLogId' => null !== $log $log->getId() : null,
  769.         ];
  770.     }
  771.     /**
  772.      * Live-Nachziehen des Zustellstatus für konkrete SMS-Log-Einträge (per Log-ID) â€” vom Batch-Panel
  773.      * der Sensor-Liste gepollt, damit die Zustellung ohne Seitenreload erscheint. Fragt nur noch
  774.      * offene (Pending/Sent) Einträge bei Jasper nach; terminale Status kommen unverändert zurück.
  775.      *
  776.      * @Route("/sensor/smsStatusBatch", name="sensor_sms_status_batch", methods={"POST"})
  777.      */
  778.     public function smsStatusBatch(Request $requestWhereEverSmsService $sms): JsonResponse
  779.     {
  780.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  781.         $payload json_decode($request->getContent(), true);
  782.         $logIds = \is_array($payload['logIds'] ?? null)
  783.             ? array_values(array_unique(array_filter(array_map('intval'$payload['logIds']))))
  784.             : [];
  785.         if (empty($logIds)) {
  786.             return new JsonResponse(['state' => 'OK''results' => []]);
  787.         }
  788.         $em $this->getDoctrine()->getManager();
  789.         $logs $this->getDoctrine()->getRepository(SmsLog::class)->findBy(['id' => $logIds]);
  790.         $updated false;
  791.         $results = [];
  792.         foreach ($logs as $log) {
  793.             $status $log->getStatus();
  794.             $open = (null === $status || \in_array($statusSmsLogRepository::OPEN_STATUSEStrue));
  795.             if ($open && null !== $log->getSmsMessageId()) {
  796.                 $detail $sms->getSmsDetail($log->getSmsMessageId());
  797.                 if ($detail['ok'] && null !== $detail['status'] && $detail['status'] !== $status) {
  798.                     $log->setStatus($detail['status']);
  799.                     $status $detail['status'];
  800.                     $updated true;
  801.                 }
  802.             }
  803.             $results[] = ['logId' => $log->getId(), 'status' => $status];
  804.         }
  805.         if ($updated) {
  806.             $em->flush();
  807.         }
  808.         return new JsonResponse(['state' => 'OK''results' => $results]);
  809.     }
  810.     /**
  811.      * Manuelles Nachziehen des Zustellstatus für die offenen SMS-Logs einer TGA (Aufzug/NEA).
  812.      *
  813.      * @Route("/sensor/smsLogRefresh/{type}/{id}", name="sms_log_refresh", methods={"POST"}, requirements={"type"="elevator|nea"})
  814.      *
  815.      * @param mixed $id
  816.      */
  817.     public function smsLogRefresh(string $type$idWhereEverSmsService $sms): JsonResponse
  818.     {
  819.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  820.         $em $this->getDoctrine()->getManager();
  821.         $repo $this->getDoctrine()->getRepository(SmsLog::class);
  822.         $since = (new \DateTime())->modify('-2 hours');
  823.         $open 'elevator' === $type
  824.             $repo->findOpenForStatusRefresh(50$since, (int) $idnull)
  825.             : $repo->findOpenForStatusRefresh(50$sincenull, (int) $id);
  826.         $updated 0;
  827.         foreach ($open as $log) {
  828.             $detail $sms->getSmsDetail($log->getSmsMessageId());
  829.             if ($detail['ok'] && null !== $detail['status'] && $detail['status'] !== $log->getStatus()) {
  830.                 $log->setStatus($detail['status']);
  831.                 ++$updated;
  832.             }
  833.         }
  834.         if ($updated 0) {
  835.             $em->flush();
  836.         }
  837.         return new JsonResponse(['state' => 'OK''msg' => $updated.' Status aktualisiert''updated' => $updated]);
  838.     }
  839.     /**
  840.      * Zustellstatus einer bereits gesendeten Reboot-SMS erneut abfragen (getSmsDetail).
  841.      *
  842.      * @Route("/sensor/smsStatusWherever/{sensor}/{smsMessageId}", name="sensor_sms_status_wherever", methods={"GET"})
  843.      *
  844.      * @param mixed $smsMessageId
  845.      */
  846.     public function smsStatusWherever(Sensor $sensor$smsMessageIdWhereEverSmsService $sms): JsonResponse
  847.     {
  848.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  849.         $detail $sms->getSmsDetail($smsMessageId);
  850.         if (!$detail['ok']) {
  851.             return new JsonResponse(['state' => 'error''msg' => $detail['error'] ?? 'Statusabfrage fehlgeschlagen']);
  852.         }
  853.         return new JsonResponse(['state' => 'OK''msg' => 'Status: '.($detail['status'] ?? 'unbekannt'), 'status' => $detail['status']]);
  854.     }
  855.     /**
  856.      * @Route("/sensor/update", name="sensor_update", methods={"GET"})
  857.      *
  858.      * @OA\Get(
  859.      *     summary="Get new observer scripts",
  860.      *
  861.      *
  862.      *     @OA\Parameter(
  863.      *         name="body",
  864.      *         description="Get data.",
  865.      *         in="body",
  866.      *         required=true,
  867.      *
  868.      *         @OA\Schema(
  869.      *
  870.      *             @OA\Property(
  871.      *                 property="serial",
  872.      *                 type="string",
  873.      *                 minLength=1,
  874.      *                 example=1
  875.      *             ),
  876.      *             @OA\Property(
  877.      *                 property="scriptType",
  878.      *                 type="string",
  879.      *                 minLength=1,
  880.      *                 example=1
  881.      *             ),
  882.      *         )
  883.      *     ),
  884.      *
  885.      *     @OA\Response(
  886.      *         response=200,
  887.      *         description="Returns status 200 and the modified contact.",
  888.      *
  889.      *         @OA\Schema(
  890.      *             type="object",
  891.      *             properties={
  892.      *
  893.      *                 @OA\Property(property="id", type="integer"),
  894.      *             }
  895.      *         )
  896.      *     ),
  897.      *
  898.      *     @OA\Response(
  899.      *         response=404,
  900.      *         description="Returns status 404 if there is no contact with the given id."
  901.      *     )
  902.      * )
  903.      */
  904.     public function sensorUpdateAction(Request $requestObserverDeployManifest $manifest)
  905.     {
  906.         $projectDir $this->appKernel->getProjectDir();
  907.         $baseDir $projectDir.'/bin/acobesmart_observer/';
  908.         $data json_decode($request->getContent());
  909.         $scriptType $data->scriptType ?? null;
  910.         $em $this->getDoctrine()->getManager();
  911.         $sensor = isset($data->serial)
  912.             ? $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial])
  913.             : null;
  914.         if (!$sensor) {
  915.             return new JsonResponse(['error' => 'Sensor nicht gefunden'], 404);
  916.         }
  917.         // Manifest: treibt das Auto-Update auf dem Pi (safe_update.py).
  918.         if ('manifest' === $scriptType) {
  919.             return new JsonResponse($manifest->build());
  920.         }
  921.         // Rueckwaerts-Kompatible Aliase (alte Pi-Scripts nutzen feste scriptType-Namen).
  922.         $aliases = [
  923.             'aco_observer' => 'aco_observer.py',
  924.             'aco_db_worker' => 'aco_db_worker.py',
  925.             'do_restart' => 'do_restart.py',
  926.             'helper' => 'helper.py',
  927.             'heartbeat' => 'heartbeat.py',
  928.             'onlineCheck' => 'online_check.py',
  929.             'safe_update' => 'safe_update.py',
  930.         ];
  931.         // Sonderfall Version: nicht Teil des Deploy-Manifests (separater .version-Handshake).
  932.         if ('version' === $scriptType) {
  933.             $filePathAbs $baseDir.'.version';
  934.         } else {
  935.             // Alias -> relativer Pfad; sonst wird scriptType direkt als relativer Pfad
  936.             // interpretiert (neue Pi-Scripts liefern den Manifest-'path').
  937.             $relPath $aliases[$scriptType] ?? (string) $scriptType;
  938.             $filePathAbs $manifest->resolveDeliverable($relPath);
  939.             if (null === $filePathAbs) {
  940.                 return new JsonResponse(['error' => 'Unbekannter scriptType'], 400);
  941.             }
  942.         }
  943.         if (!is_file($filePathAbs)) {
  944.             return new JsonResponse(['error' => 'Datei nicht vorhanden'], 404);
  945.         }
  946.         $fileContent file_get_contents($filePathAbs);
  947.         $response = new Response($fileContent);
  948.         $response->headers->set('Cache-Control''private');
  949.         $response->headers->set('Content-type''text/plain');
  950.         $response->headers->set('Content-length', (string) strlen($fileContent));
  951.         return $response;
  952.     }
  953.     /**
  954.      * Gets a list of sensors.
  955.      *
  956.      * @Rest\Get("/sensorsQrPrint")
  957.      */
  958.     public function sensorsQrPrintAction(Request $request)
  959.     {
  960.         $data = [];
  961.         $sortField false;
  962.         $sortOrder false;
  963.         $limit 20;
  964.         $currentPage 1;
  965.         $pagination = ($request->get('pagination') ? $request->get('pagination') : false);
  966.         if ($pagination) {
  967.             $limit $pagination['perpage'];
  968.             $currentPage $pagination['page'];
  969.         }
  970.         $sortField = ($request->get('sort') ? $request->get('sort')['field'] : 'updatedAt');
  971.         $sortOrder = ($request->get('sort') ? $request->get('sort')['sort'] : 'DESC');
  972.         $query = ($request->get('query') ? $request->get('query') : []);
  973.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  974.         // findByCompany($user, $states = false, $tankStates = false, $sensorState = false, $limitSort = [], $isInstalled = true)
  975.         $limitSortQuery = [
  976.             'sortField' => $sortField,
  977.             'sortOrder' => $sortOrder,
  978.             'filter' => $query,
  979.             'limit' => $limit,
  980.             'currentPage' => $currentPage,
  981.         ];
  982.         $serializedSensors = [];
  983.         $senorsComplete $repository->getUninstalled($limitSortQuerytrue);
  984.         $pages round($senorsComplete $limit);
  985.         $sensors $repository->getUninstalled($limitSortQueryfalse);
  986.         foreach ($sensors as $sensor) {
  987.             $serializedSensors[] = $this->serializeSensors($sensortrue);
  988.         }
  989.         $data['data'] = $serializedSensors;
  990.         $data['meta'] = [
  991.             'page' => $currentPage,
  992.             'pages' => $pages,
  993.             'perpage' => $limit,
  994.             'total' => $senorsComplete,
  995.             'sort' => $sortOrder,
  996.             'field' => $sortField,
  997.         ];
  998.         $response = new Response(json_encode($data), 200);
  999.         $response->headers->set('Content-Type''application/json');
  1000.         return $response;
  1001.     }
  1002.     /**
  1003.      * Server-side Datenquelle für die Sensor-Liste (KTDatatable): liefert {data, meta}.
  1004.      * Spalten: id, serial, phone, sim_number, state/stateLabel, address (Aufzug/NEA).
  1005.      *
  1006.      * @Rest\Get("/sensorsList", name="api_sensors_list")
  1007.      */
  1008.     public function sensorsListAction(Request $request)
  1009.     {
  1010.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  1011.         $limit 20;
  1012.         $currentPage 1;
  1013.         $pagination $request->get('pagination') ?: false;
  1014.         if ($pagination) {
  1015.             $limit = (int) $pagination['perpage'];
  1016.             $currentPage = (int) $pagination['page'];
  1017.         }
  1018.         $sortField $request->get('sort') ? $request->get('sort')['field'] : 'id';
  1019.         $sortOrder $request->get('sort') ? $request->get('sort')['sort'] : 'DESC';
  1020.         $query $request->get('query') ?: [];
  1021.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1022.         $limitSortQuery = [
  1023.             'sortField' => $sortField,
  1024.             'sortOrder' => $sortOrder,
  1025.             'filter' => $query,
  1026.             'limit' => $limit,
  1027.             'currentPage' => $currentPage,
  1028.         ];
  1029.         $total $repository->getFilteredForList($limitSortQuerytrue);
  1030.         $pages $limit ? (int) ceil($total $limit) : 1;
  1031.         $sensors $repository->getFilteredForList($limitSortQueryfalse);
  1032.         // Jüngsten Reboot-SMS-Zustellstatus je angezeigtem Sensor nachladen (nur diese Seite).
  1033.         $sensorIds array_map(function ($s) { return $s->getId(); }, $sensors);
  1034.         $latestSms $this->getDoctrine()->getRepository(SmsLog::class)->latestBySensorIds($sensorIds);
  1035.         $rows = [];
  1036.         foreach ($sensors as $sensor) {
  1037.             $elevator $sensor->getElevator();
  1038.             $nea $sensor->getNea();
  1039.             $address null;
  1040.             if (null !== $elevator && null !== $elevator->getAddress()) {
  1041.                 $address $elevator->getAddress();
  1042.             } elseif (null !== $nea && method_exists($nea'getAddress') && null !== $nea->getAddress()) {
  1043.                 $address $nea->getAddress();
  1044.             }
  1045.             $addressStr '—';
  1046.             if (null !== $address) {
  1047.                 $addressStr trim(trim(sprintf('%s %s, %s %s'$address->getStreet(), $address->getStreetNumber(), $address->getPlz(), $address->getLocation())), ' ,') ?: '—';
  1048.             }
  1049.             $labels $sensor->getStateNames();
  1050.             $sms $latestSms[$sensor->getId()] ?? null;
  1051.             $rows[] = [
  1052.                 'id' => $sensor->getId(),
  1053.                 'serial' => $sensor->getSerial(),
  1054.                 'phone' => $sensor->getPhone(),
  1055.                 'sim_number' => $sensor->getSimNumber(),
  1056.                 'state' => $sensor->getState(),
  1057.                 'stateLabel' => $labels[$sensor->getState()] ?? (string) $sensor->getState(),
  1058.                 'address' => $addressStr,
  1059.                 'elevatorId' => null !== $elevator $elevator->getId() : null,
  1060.                 'neaId' => null !== $nea $nea->getId() : null,
  1061.                 'smsStatus' => null !== $sms $sms['status'] : null,
  1062.                 'smsLogId' => null !== $sms $sms['logId'] : null,
  1063.                 'smsCommand' => null !== $sms $sms['command'] : null,
  1064.                 'smsAt' => (null !== $sms && !empty($sms['createdAt'])) ? date('d.m.Y H:i'strtotime((string) $sms['createdAt'])) : null,
  1065.             ];
  1066.         }
  1067.         $data = ['data' => $rows'meta' => [
  1068.             'page' => $currentPage'pages' => $pages'perpage' => $limit,
  1069.             'total' => $total'sort' => $sortOrder'field' => $sortField,
  1070.         ]];
  1071.         $response = new Response(json_encode($data), 200);
  1072.         $response->headers->set('Content-Type''application/json');
  1073.         return $response;
  1074.     }
  1075.     /**
  1076.      * Gets a list of sensors.
  1077.      *
  1078.      * @Rest\Get("/getSignalAndProvider/{tgaHash}")
  1079.      *
  1080.      * @param mixed $tgaHash
  1081.      */
  1082.     public function getSignalAndProvider(Request $request$tgaHash)
  1083.     {
  1084.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1085.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1086.             $signal $this->sensorRemoteService->getSignalQuality($sensor);
  1087.             $provider $this->sensorRemoteService->getProvider($sensor);
  1088.             $response = ['signalQuality' => $this->sensorRemoteService->signalQualityToString($signal).'('.$signal.')''provider' => $provider'error' => false'errorMsg' => ''];
  1089.         } else {
  1090.             $response = ['signalQuality' => '''provider' => '''error' => true'errorMsg' => 'Sensor not found!'];
  1091.         }
  1092.         return new JsonResponse($response);
  1093.     }
  1094.     /**
  1095.      * Gets a list of sensors.
  1096.      *
  1097.      * @Rest\Get("/getSignal/{tgaHash}", name="get_signal")
  1098.      *
  1099.      * @param mixed $tgaHash
  1100.      */
  1101.     public function getSignal(Request $request$tgaHash)
  1102.     {
  1103.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1104.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1105.             $signal $this->sensorRemoteService->getSignalQuality($sensor);
  1106.             $response = ['signalQuality' => $this->sensorRemoteService->signalQualityToString($signal).'('.$signal.')''error' => false'errorMsg' => ''];
  1107.         } else {
  1108.             $response = ['signalQuality' => 'N/A''error' => true'errorMsg' => 'Sensor not found!'];
  1109.         }
  1110.         return new JsonResponse($response);
  1111.     }
  1112.     /**
  1113.      * Is sensor online.
  1114.      *
  1115.      * @Rest\Get("/isOnline/{tgaHash}", name="is_online")
  1116.      *
  1117.      * @param mixed $tgaHash
  1118.      */
  1119.     public function isOnline(Request $request$tgaHash)
  1120.     {
  1121.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1122.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1123.             $isOnline $this->sensorRemoteService->checkSSHConnection($sensor);
  1124.             // updateRunning: laeuft gerade ein Software-Update? -> InstallTool zeigt "Update laeuft"
  1125.             // statt "offline", damit der Monteur den Sensor nicht resettet. updateStatus: letzter
  1126.             // Zustand (v.a. failed_network=Empfangsprobleme), falls die Seite erst nach dem Live-
  1127.             // Event via Mercure geoeffnet/neu geladen wird.
  1128.             $response = [
  1129.                 'isOnline' => $isOnline,
  1130.                 'updateRunning' => $sensor->isUpdateRunning(),
  1131.                 'updateStatus' => $sensor->getUpdateStatus(),
  1132.                 'errorMsg' => '',
  1133.             ];
  1134.         } else {
  1135.             $response = ['isOnline' => false'updateRunning' => false'updateStatus' => null'errorMsg' => 'Sensor not found!'];
  1136.         }
  1137.         return new JsonResponse($response);
  1138.     }
  1139.     /**
  1140.      * Meldung der Update-Scripts (safe_update.py): "running" beim Start, "done"/"failed" am Ende.
  1141.      * Setzt sensor.update_started_at und pusht das InstallTool-Live-Topic.
  1142.      *
  1143.      * @Route("/sensor/updateStatus", name="sensor_update_status", methods={"PUT"})
  1144.      */
  1145.     public function updateStatusAction(Request $request)
  1146.     {
  1147.         $data json_decode($request->getContent());
  1148.         $em $this->getDoctrine()->getManager();
  1149.         $sensor = isset($data->serial)
  1150.             ? $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial])
  1151.             : null;
  1152.         if (!$sensor) {
  1153.             return new JsonResponse(['error' => 'Sensor nicht gefunden'], 404);
  1154.         }
  1155.         $status $data->status ?? '';
  1156.         $attempt = isset($data->attempt) ? (int) $data->attempt null;
  1157.         $maxAttempts = isset($data->max_attempts) ? (int) $data->max_attempts null;
  1158.         // running/retry -> Update laeuft: Timer setzen/verlaengern (isUpdateRunning -> "laeuft").
  1159.         // done -> fertig, Feld raus. failed*/sonst -> Timer aus, aber Zustand bleibt persistent,
  1160.         // damit ein spaeter geoeffnetes InstallTool den Ausgang (v.a. Empfangsprobleme) noch zeigt.
  1161.         $running in_array($status, ['running''retry'], true);
  1162.         $sensor->setUpdateStartedAt($running ? new \DateTime() : null);
  1163.         $sensor->setUpdateStatus('done' === $status null : ($status ?: null));
  1164.         $em->flush();
  1165.         try {
  1166.             $this->hub->publish(new Update(
  1167.                 'https://acobesmart.com/sensorinstall_'.$this->appKernel->getEnvironment().'_'.$sensor->getId(),
  1168.                 json_encode(['type' => 'update''status' => $status'attempt' => $attempt'max' => $maxAttempts])
  1169.             ));
  1170.         } catch (\Throwable $e) {
  1171.             $this->logger->error('updateStatus mercure: '.$e->getMessage());
  1172.         }
  1173.         return new JsonResponse(['ok' => 'ok']);
  1174.     }
  1175.     /**
  1176.      * Is sensor online.
  1177.      *
  1178.      * @Rest\Get("/clearSensorDatabase/{tgaHash}", name="clear_sensor_database")
  1179.      *
  1180.      * @param mixed $tgaHash
  1181.      */
  1182.     public function clearSensorDatabase(Request $request$tgaHash)
  1183.     {
  1184.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1185.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1186.             $cleared $this->sensorRemoteService->clearSensorDatabase($sensor);
  1187.             $this->sensorRemoteService->restartAllScripts($sensor);
  1188.             $response = ['cleared' => $cleared'errorMsg' => ''];
  1189.         } else {
  1190.             $response = ['cleared' => false'errorMsg' => 'Error on clearing sensor DB.'];
  1191.         }
  1192.         return new JsonResponse($response);
  1193.     }
  1194.     /**
  1195.      * Gets a list of sensors.
  1196.      *
  1197.      * @Rest\Get("/getProvider/{tgaHash}", name="get_provider")
  1198.      *
  1199.      * @param mixed $tgaHash
  1200.      */
  1201.     public function getProvider(Request $request$tgaHash)
  1202.     {
  1203.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1204.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1205.             $provider $this->sensorRemoteService->getProvider($sensor);
  1206.             $response = ['provider' => $provider'error' => false'errorMsg' => ''];
  1207.         } else {
  1208.             $response = ['provider' => 'N/A''error' => true'errorMsg' => 'Sensor not found!'];
  1209.         }
  1210.         return new JsonResponse($response);
  1211.     }
  1212.     private function serializeSensors(Sensor $sensor)
  1213.     {
  1214.         $serialized = [
  1215.             'id' => $sensor->getId(),
  1216.             'sensorId' => $sensor->getId(),
  1217.             'createdAt' => $sensor->getCreatedAt()->format('d.m.Y H:i:s'),
  1218.             'stateName' => $sensor->getStateName(),
  1219.             'typeOfState' => $sensor->getTypeOfState(),
  1220.             'lastOnline' => ($sensor->getLastOnline() ? $sensor->getLastOnline()->format('d.m.Y H:i:s') : 'N/A'),
  1221.             'observerState' => $sensor->getObserverStateName(),
  1222.             'typeOfObserverState' => $sensor->getTypeOfObserverState(),
  1223.             'observerLastOnline' => ($sensor->getObserverLastOnline() ? $sensor->getObserverLastOnline()->format('d.m.Y H:i:s') : 'N/A'),
  1224.             'observerVersion' => $sensor->getObserverVersion(),
  1225.             'serial' => $sensor->getSerial(),
  1226.             'port' => $sensor->getPort(),
  1227.         ];
  1228.         $actionArray = [];
  1229.         $actionArray['showSensorqrCode'] = $this->generateUrl('show_sensor_qr_coed', ['sensorHash' => $sensor->getTgaHash()]);
  1230.         $serialized['actions'] = $actionArray;
  1231.         return $serialized;
  1232.     }
  1233. }