src/Controller/Api/SensorController.php line 225

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/observerVersion", name="observer_version", methods={"PUT"})
  469.      *
  470.      * @OA\Put(
  471.      *     summary="Set version of script running at sensor.",
  472.      *
  473.      *
  474.      *     @OA\Parameter(
  475.      *         name="body",
  476.      *         description="Post data.",
  477.      *         in="body",
  478.      *         required=true,
  479.      *
  480.      *         @OA\Schema(
  481.      *             type="string",
  482.      *             required={"serial", "version"},
  483.      *
  484.      *             @OA\Property(
  485.      *                 property="serial",
  486.      *                 type="string",
  487.      *                 minLength=1,
  488.      *                 example=1
  489.      *             ),
  490.      *             @OA\Property(
  491.      *                 property="version",
  492.      *                 type="string",
  493.      *                 minLength=1,
  494.      *                 example=1
  495.      *             ),
  496.      *         )
  497.      *     ),
  498.      *
  499.      *     @OA\Response(
  500.      *         response=200,
  501.      *         description="Returns status 200 and the modified contact.",
  502.      *
  503.      *         @OA\Schema(
  504.      *             type="object",
  505.      *             properties={
  506.      *
  507.      *                 @OA\Property(property="id", type="integer"),
  508.      *             }
  509.      *         )
  510.      *     ),
  511.      *
  512.      *     @OA\Response(
  513.      *         response=404,
  514.      *         description="Returns status 404 if there is no contact with the given id."
  515.      *     )
  516.      * )
  517.      */
  518.     public function observerVersionAction(Request $request): JsonResponse
  519.     {
  520.         $requestBody $request->getContent();
  521.         $data json_decode($requestBody);
  522.         $em $this->getDoctrine()->getManager();
  523.         $sensorRepo $em->getRepository(Sensor::class);
  524.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial]);
  525.         // Manche Observer melden die Version mit angehängtem Zeilenumbruch aus der .version-Datei
  526.         $version trim($data->version);
  527.         if ($sensor) {
  528.             $sensor->setObserverVersion($version);
  529.             $em->persist($sensor);
  530.             $em->flush();
  531.             $responseArray = ['status' => 'ok'];
  532.         } else {
  533.             $responseArray = ['error' => 'Sensor '.$data->serial.' not found'];
  534.         }
  535.         return new JsonResponse($responseArray);
  536.     }
  537.     /**
  538.      * @Route("/sensor/getSensorId", name="get_sensor_id", methods={"GET"})
  539.      *
  540.      * @OA\Get(
  541.      *     summary="Get internal ID of sensor",
  542.      *
  543.      *
  544.      *     @OA\Parameter(
  545.      *         name="body",
  546.      *         description="Post data.",
  547.      *         in="body",
  548.      *         required=true,
  549.      *
  550.      *         @OA\Schema(
  551.      *             type="string",
  552.      *             required={"serial"},
  553.      *
  554.      *             @OA\Property(
  555.      *                 property="serial",
  556.      *                 type="string",
  557.      *                 minLength=1,
  558.      *                 example=1
  559.      *             ),
  560.      *         )
  561.      *     ),
  562.      *
  563.      *     @OA\Response(
  564.      *         response=200,
  565.      *         description="Returns status 200 and the modified contact.",
  566.      *
  567.      *         @OA\Schema(
  568.      *             type="object",
  569.      *             properties={
  570.      *
  571.      *                 @OA\Property(property="id", type="integer"),
  572.      *             }
  573.      *         )
  574.      *     ),
  575.      *
  576.      *     @OA\Response(
  577.      *         response=404,
  578.      *         description="Returns status 404 if there is no contact with the given id."
  579.      *     )
  580.      * )
  581.      */
  582.     public function getSensorIdAction(Request $request): JsonResponse
  583.     {
  584.         $requestBody $request->getContent();
  585.         $data json_decode($requestBody);
  586.         $em $this->getDoctrine()->getManager();
  587.         $sensorRepo $em->getRepository(Sensor::class);
  588.         $sensor $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial]);
  589.         if ($sensor) {
  590.             $responseArray = ['id' => $sensor->getId()];
  591.         } else {
  592.             $responseArray = ['error' => 'Sensor '.$data->serial.' not found'];
  593.         }
  594.         return new JsonResponse($responseArray);
  595.     }
  596.     /**
  597.      * @Route("/sensor/sendSMS/{action}/{sensor}", name="sensor_send_sms", methods={"POST"})
  598.      *
  599.      * @param mixed $action
  600.      * @param mixed $sensor
  601.      */
  602.     public function sendSMS($actionSensor $sensor)
  603.     {
  604.         if ($sensor->getPhone()) {
  605.             $client HttpClient::create();
  606.             switch ($action) {
  607.                 case 'test':
  608.                     $query['action'] = 'Test';
  609.                     break;
  610.                 case 'reboot':
  611.                     $query['action'] = 'Reboot';
  612.                     break;
  613.                 case 'restartObserver':
  614.                     $query['action'] = 'Restart';
  615.                     break;
  616.                 case 'reconnect':
  617.                     $query['action'] = 'Reconnect';
  618.                     break;
  619.             }
  620.             $query = ['phone' => $sensor->getPhone(), 'action' => $query['action']];
  621.             $response $client->request('POST''http://acosms.duckdns.org:8878/index.php', [
  622.                 // these values are automatically encoded before including them in the URL
  623.                 'body' => $query,
  624.             ]);
  625.             if ('OK' == trim($response->getContent())) {
  626.                 $response = ['state' => 'OK''msg' => 'OK'];
  627.             } else {
  628.                 $response = ['state' => 'error''msg' => trim($response->getContent())];
  629.             }
  630.         } else {
  631.             $response = ['state' => 'error''msg' => 'No phone number given!'];
  632.         }
  633.         return new JsonResponse($response);
  634.     }
  635.     /** Sicherheits-Deckel gegen Massenversand im Batch. */
  636.     private const SMS_BATCH_MAX 100;
  637.     /**
  638.      * Reboot-SMS Ã¼ber WhereEver/Jasper an die ICCID des Sensors (günstiger Weg als normale SMS),
  639.      * Abfrage des Zustellstatus (getSmsDetail) und Protokollierung im SMS-Log.
  640.      *
  641.      * @Route("/sensor/rebootSmsWherever/{sensor}", name="sensor_reboot_sms_wherever", methods={"POST"})
  642.      */
  643.     public function rebootSmsWherever(Sensor $sensorWhereEverSmsService $smsSmsLogger $smsLogger): JsonResponse
  644.     {
  645.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  646.         return new JsonResponse($this->doCommandSms($sensor'Reboot'$sms$smsLogger));
  647.     }
  648.     /**
  649.      * Steuer-SMS mit frei wählbarem Kommando. Sinnvoll, weil ein Reboot nur bei flüchtigen
  650.      * Störungen hilft: Läuft der Pi noch, sind aber die Observer-Dienste gestorben oder hängt
  651.      * die Einwahl, wirken "Restart" bzw. "Reconnect" gezielter. Zulässige Werte stehen in
  652.      * WhereEverSmsService::COMMANDS; alles andere wird abgewiesen, bevor Kosten entstehen.
  653.      *
  654.      * @Route("/sensor/commandSmsWherever/{sensor}/{command}", name="sensor_command_sms_wherever", methods={"POST"})
  655.      */
  656.     public function commandSmsWherever(Sensor $sensorstring $commandWhereEverSmsService $smsSmsLogger $smsLogger): JsonResponse
  657.     {
  658.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  659.         return new JsonResponse($this->doCommandSms($sensor$command$sms$smsLogger));
  660.     }
  661.     /**
  662.      * Batch: Reboot-SMS an alle ausgewählten Sensoren (per Checkbox aus der Sensor-Liste).
  663.      *
  664.      * @Route("/sensor/rebootSmsBatch", name="sensor_reboot_sms_batch", methods={"POST"})
  665.      */
  666.     public function rebootSmsBatch(Request $requestWhereEverSmsService $smsSmsLogger $smsLogger): JsonResponse
  667.     {
  668.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  669.         $payload json_decode($request->getContent(), true);
  670.         $ids = \is_array($payload['ids'] ?? null) ? $payload['ids'] : [];
  671.         $ids array_values(array_unique(array_filter(array_map('intval'$ids))));
  672.         // Ohne Angabe bleibt es beim bisherigen Verhalten (Reboot)
  673.         $command = (string) ($payload['command'] ?? 'Reboot');
  674.         if (empty($ids)) {
  675.             return new JsonResponse(['state' => 'error''msg' => 'Keine Sensoren ausgewählt.']);
  676.         }
  677.         if (!isset(WhereEverSmsService::COMMANDS[$command])) {
  678.             return new JsonResponse(['state' => 'error''msg' => 'Unbekanntes Kommando: '.$command]);
  679.         }
  680.         if (\count($ids) > self::SMS_BATCH_MAX) {
  681.             return new JsonResponse(['state' => 'error''msg' => 'Zu viele Sensoren ausgewählt (max. '.self::SMS_BATCH_MAX.').']);
  682.         }
  683.         $repo $this->getDoctrine()->getRepository(Sensor::class);
  684.         $sent 0;
  685.         $failed 0;
  686.         $results = [];
  687.         foreach ($ids as $id) {
  688.             $sensor $repo->find($id);
  689.             if (null === $sensor) {
  690.                 ++$failed;
  691.                 $results[] = ['id' => $id'ok' => false'error' => 'Sensor nicht gefunden'];
  692.                 continue;
  693.             }
  694.             $r $this->doCommandSms($sensor$command$sms$smsLogger);
  695.             $ok 'OK' === $r['state'];
  696.             $ok ? ++$sent : ++$failed;
  697.             $results[] = [
  698.                 'id' => $id'ok' => $ok'status' => $r['status'] ?? null,
  699.                 'smsLogId' => $r['smsLogId'] ?? null'smsMessageId' => $r['smsMessageId'] ?? null,
  700.                 'error' => $ok null : ($r['msg'] ?? null),
  701.             ];
  702.         }
  703.         return new JsonResponse([
  704.             'state' => 'OK',
  705.             'msg' => sprintf('%d gesendet, %d fehlgeschlagen'$sent$failed),
  706.             'sent' => $sent'failed' => $failed'results' => $results,
  707.         ]);
  708.     }
  709.     /**
  710.      * Sendet die Steuer-SMS, ermittelt den Sofort-Status und schreibt einen SMS-Log-Eintrag.
  711.      * Gemeinsame Logik von Einzel- und Batch-Versand. Das Kommando landet unverändert im
  712.      * Log (Feld messageText), damit später nachvollziehbar ist, was geschickt wurde.
  713.      *
  714.      * @return array{state: string, msg: string, smsMessageId: mixed, status: string|null, smsLogId: int|null}
  715.      */
  716.     private function doCommandSms(Sensor $sensorstring $commandWhereEverSmsService $smsSmsLogger $smsLogger): array
  717.     {
  718.         $sentBy $this->getUser() ? (string) $this->getUser()->getUsername() : null;
  719.         $send $sms->sendCommand($sensor$command);
  720.         if (!$send['ok']) {
  721.             $log $smsLogger->log($sensor$commandnull'Fehler'$sentBy);
  722.             return ['state' => 'error''msg' => $send['error'] ?? 'Versand fehlgeschlagen''smsMessageId' => null'status' => null'smsLogId' => null !== $log $log->getId() : null];
  723.         }
  724.         $status null;
  725.         if (null !== $send['smsMessageId']) {
  726.             $detail $sms->getSmsDetail($send['smsMessageId']);
  727.             $status $detail['ok'] ? $detail['status'] : null;
  728.         }
  729.         $log $smsLogger->log($sensor$commandnull !== $send['smsMessageId'] ? (string) $send['smsMessageId'] : null$status$sentBy);
  730.         return [
  731.             'state' => 'OK',
  732.             'msg' => $command.'-SMS gesendet'.(null !== $status ' (Status: '.$status.')' ''),
  733.             'smsMessageId' => $send['smsMessageId'],
  734.             'status' => $status,
  735.             'smsLogId' => null !== $log $log->getId() : null,
  736.         ];
  737.     }
  738.     /**
  739.      * Live-Nachziehen des Zustellstatus für konkrete SMS-Log-Einträge (per Log-ID) â€” vom Batch-Panel
  740.      * der Sensor-Liste gepollt, damit die Zustellung ohne Seitenreload erscheint. Fragt nur noch
  741.      * offene (Pending/Sent) Einträge bei Jasper nach; terminale Status kommen unverändert zurück.
  742.      *
  743.      * @Route("/sensor/smsStatusBatch", name="sensor_sms_status_batch", methods={"POST"})
  744.      */
  745.     public function smsStatusBatch(Request $requestWhereEverSmsService $sms): JsonResponse
  746.     {
  747.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  748.         $payload json_decode($request->getContent(), true);
  749.         $logIds = \is_array($payload['logIds'] ?? null)
  750.             ? array_values(array_unique(array_filter(array_map('intval'$payload['logIds']))))
  751.             : [];
  752.         if (empty($logIds)) {
  753.             return new JsonResponse(['state' => 'OK''results' => []]);
  754.         }
  755.         $em $this->getDoctrine()->getManager();
  756.         $logs $this->getDoctrine()->getRepository(SmsLog::class)->findBy(['id' => $logIds]);
  757.         $updated false;
  758.         $results = [];
  759.         foreach ($logs as $log) {
  760.             $status $log->getStatus();
  761.             $open = (null === $status || \in_array($statusSmsLogRepository::OPEN_STATUSEStrue));
  762.             if ($open && null !== $log->getSmsMessageId()) {
  763.                 $detail $sms->getSmsDetail($log->getSmsMessageId());
  764.                 if ($detail['ok'] && null !== $detail['status'] && $detail['status'] !== $status) {
  765.                     $log->setStatus($detail['status']);
  766.                     $status $detail['status'];
  767.                     $updated true;
  768.                 }
  769.             }
  770.             $results[] = ['logId' => $log->getId(), 'status' => $status];
  771.         }
  772.         if ($updated) {
  773.             $em->flush();
  774.         }
  775.         return new JsonResponse(['state' => 'OK''results' => $results]);
  776.     }
  777.     /**
  778.      * Manuelles Nachziehen des Zustellstatus für die offenen SMS-Logs einer TGA (Aufzug/NEA).
  779.      *
  780.      * @Route("/sensor/smsLogRefresh/{type}/{id}", name="sms_log_refresh", methods={"POST"}, requirements={"type"="elevator|nea"})
  781.      *
  782.      * @param mixed $id
  783.      */
  784.     public function smsLogRefresh(string $type$idWhereEverSmsService $sms): JsonResponse
  785.     {
  786.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  787.         $em $this->getDoctrine()->getManager();
  788.         $repo $this->getDoctrine()->getRepository(SmsLog::class);
  789.         $since = (new \DateTime())->modify('-2 hours');
  790.         $open 'elevator' === $type
  791.             $repo->findOpenForStatusRefresh(50$since, (int) $idnull)
  792.             : $repo->findOpenForStatusRefresh(50$sincenull, (int) $id);
  793.         $updated 0;
  794.         foreach ($open as $log) {
  795.             $detail $sms->getSmsDetail($log->getSmsMessageId());
  796.             if ($detail['ok'] && null !== $detail['status'] && $detail['status'] !== $log->getStatus()) {
  797.                 $log->setStatus($detail['status']);
  798.                 ++$updated;
  799.             }
  800.         }
  801.         if ($updated 0) {
  802.             $em->flush();
  803.         }
  804.         return new JsonResponse(['state' => 'OK''msg' => $updated.' Status aktualisiert''updated' => $updated]);
  805.     }
  806.     /**
  807.      * Zustellstatus einer bereits gesendeten Reboot-SMS erneut abfragen (getSmsDetail).
  808.      *
  809.      * @Route("/sensor/smsStatusWherever/{sensor}/{smsMessageId}", name="sensor_sms_status_wherever", methods={"GET"})
  810.      *
  811.      * @param mixed $smsMessageId
  812.      */
  813.     public function smsStatusWherever(Sensor $sensor$smsMessageIdWhereEverSmsService $sms): JsonResponse
  814.     {
  815.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  816.         $detail $sms->getSmsDetail($smsMessageId);
  817.         if (!$detail['ok']) {
  818.             return new JsonResponse(['state' => 'error''msg' => $detail['error'] ?? 'Statusabfrage fehlgeschlagen']);
  819.         }
  820.         return new JsonResponse(['state' => 'OK''msg' => 'Status: '.($detail['status'] ?? 'unbekannt'), 'status' => $detail['status']]);
  821.     }
  822.     /**
  823.      * @Route("/sensor/update", name="sensor_update", methods={"GET"})
  824.      *
  825.      * @OA\Get(
  826.      *     summary="Get new observer scripts",
  827.      *
  828.      *
  829.      *     @OA\Parameter(
  830.      *         name="body",
  831.      *         description="Get data.",
  832.      *         in="body",
  833.      *         required=true,
  834.      *
  835.      *         @OA\Schema(
  836.      *
  837.      *             @OA\Property(
  838.      *                 property="serial",
  839.      *                 type="string",
  840.      *                 minLength=1,
  841.      *                 example=1
  842.      *             ),
  843.      *             @OA\Property(
  844.      *                 property="scriptType",
  845.      *                 type="string",
  846.      *                 minLength=1,
  847.      *                 example=1
  848.      *             ),
  849.      *         )
  850.      *     ),
  851.      *
  852.      *     @OA\Response(
  853.      *         response=200,
  854.      *         description="Returns status 200 and the modified contact.",
  855.      *
  856.      *         @OA\Schema(
  857.      *             type="object",
  858.      *             properties={
  859.      *
  860.      *                 @OA\Property(property="id", type="integer"),
  861.      *             }
  862.      *         )
  863.      *     ),
  864.      *
  865.      *     @OA\Response(
  866.      *         response=404,
  867.      *         description="Returns status 404 if there is no contact with the given id."
  868.      *     )
  869.      * )
  870.      */
  871.     public function sensorUpdateAction(Request $requestObserverDeployManifest $manifest)
  872.     {
  873.         $projectDir $this->appKernel->getProjectDir();
  874.         $baseDir $projectDir.'/bin/acobesmart_observer/';
  875.         $data json_decode($request->getContent());
  876.         $scriptType $data->scriptType ?? null;
  877.         $em $this->getDoctrine()->getManager();
  878.         $sensor = isset($data->serial)
  879.             ? $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial])
  880.             : null;
  881.         if (!$sensor) {
  882.             return new JsonResponse(['error' => 'Sensor nicht gefunden'], 404);
  883.         }
  884.         // Manifest: treibt das Auto-Update auf dem Pi (safe_update.py).
  885.         if ('manifest' === $scriptType) {
  886.             return new JsonResponse($manifest->build());
  887.         }
  888.         // Rueckwaerts-Kompatible Aliase (alte Pi-Scripts nutzen feste scriptType-Namen).
  889.         $aliases = [
  890.             'aco_observer' => 'aco_observer.py',
  891.             'aco_db_worker' => 'aco_db_worker.py',
  892.             'do_restart' => 'do_restart.py',
  893.             'helper' => 'helper.py',
  894.             'heartbeat' => 'heartbeat.py',
  895.             'onlineCheck' => 'online_check.py',
  896.             'safe_update' => 'safe_update.py',
  897.         ];
  898.         // Sonderfall Version: nicht Teil des Deploy-Manifests (separater .version-Handshake).
  899.         if ('version' === $scriptType) {
  900.             $filePathAbs $baseDir.'.version';
  901.         } else {
  902.             // Alias -> relativer Pfad; sonst wird scriptType direkt als relativer Pfad
  903.             // interpretiert (neue Pi-Scripts liefern den Manifest-'path').
  904.             $relPath $aliases[$scriptType] ?? (string) $scriptType;
  905.             $filePathAbs $manifest->resolveDeliverable($relPath);
  906.             if (null === $filePathAbs) {
  907.                 return new JsonResponse(['error' => 'Unbekannter scriptType'], 400);
  908.             }
  909.         }
  910.         if (!is_file($filePathAbs)) {
  911.             return new JsonResponse(['error' => 'Datei nicht vorhanden'], 404);
  912.         }
  913.         $fileContent file_get_contents($filePathAbs);
  914.         $response = new Response($fileContent);
  915.         $response->headers->set('Cache-Control''private');
  916.         $response->headers->set('Content-type''text/plain');
  917.         $response->headers->set('Content-length', (string) strlen($fileContent));
  918.         return $response;
  919.     }
  920.     /**
  921.      * Gets a list of sensors.
  922.      *
  923.      * @Rest\Get("/sensorsQrPrint")
  924.      */
  925.     public function sensorsQrPrintAction(Request $request)
  926.     {
  927.         $data = [];
  928.         $sortField false;
  929.         $sortOrder false;
  930.         $limit 20;
  931.         $currentPage 1;
  932.         $pagination = ($request->get('pagination') ? $request->get('pagination') : false);
  933.         if ($pagination) {
  934.             $limit $pagination['perpage'];
  935.             $currentPage $pagination['page'];
  936.         }
  937.         $sortField = ($request->get('sort') ? $request->get('sort')['field'] : 'updatedAt');
  938.         $sortOrder = ($request->get('sort') ? $request->get('sort')['sort'] : 'DESC');
  939.         $query = ($request->get('query') ? $request->get('query') : []);
  940.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  941.         // findByCompany($user, $states = false, $tankStates = false, $sensorState = false, $limitSort = [], $isInstalled = true)
  942.         $limitSortQuery = [
  943.             'sortField' => $sortField,
  944.             'sortOrder' => $sortOrder,
  945.             'filter' => $query,
  946.             'limit' => $limit,
  947.             'currentPage' => $currentPage,
  948.         ];
  949.         $serializedSensors = [];
  950.         $senorsComplete $repository->getUninstalled($limitSortQuerytrue);
  951.         $pages round($senorsComplete $limit);
  952.         $sensors $repository->getUninstalled($limitSortQueryfalse);
  953.         foreach ($sensors as $sensor) {
  954.             $serializedSensors[] = $this->serializeSensors($sensortrue);
  955.         }
  956.         $data['data'] = $serializedSensors;
  957.         $data['meta'] = [
  958.             'page' => $currentPage,
  959.             'pages' => $pages,
  960.             'perpage' => $limit,
  961.             'total' => $senorsComplete,
  962.             'sort' => $sortOrder,
  963.             'field' => $sortField,
  964.         ];
  965.         $response = new Response(json_encode($data), 200);
  966.         $response->headers->set('Content-Type''application/json');
  967.         return $response;
  968.     }
  969.     /**
  970.      * Server-side Datenquelle für die Sensor-Liste (KTDatatable): liefert {data, meta}.
  971.      * Spalten: id, serial, phone, sim_number, state/stateLabel, address (Aufzug/NEA).
  972.      *
  973.      * @Rest\Get("/sensorsList", name="api_sensors_list")
  974.      */
  975.     public function sensorsListAction(Request $request)
  976.     {
  977.         $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
  978.         $limit 20;
  979.         $currentPage 1;
  980.         $pagination $request->get('pagination') ?: false;
  981.         if ($pagination) {
  982.             $limit = (int) $pagination['perpage'];
  983.             $currentPage = (int) $pagination['page'];
  984.         }
  985.         $sortField $request->get('sort') ? $request->get('sort')['field'] : 'id';
  986.         $sortOrder $request->get('sort') ? $request->get('sort')['sort'] : 'DESC';
  987.         $query $request->get('query') ?: [];
  988.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  989.         $limitSortQuery = [
  990.             'sortField' => $sortField,
  991.             'sortOrder' => $sortOrder,
  992.             'filter' => $query,
  993.             'limit' => $limit,
  994.             'currentPage' => $currentPage,
  995.         ];
  996.         $total $repository->getFilteredForList($limitSortQuerytrue);
  997.         $pages $limit ? (int) ceil($total $limit) : 1;
  998.         $sensors $repository->getFilteredForList($limitSortQueryfalse);
  999.         // Jüngsten Reboot-SMS-Zustellstatus je angezeigtem Sensor nachladen (nur diese Seite).
  1000.         $sensorIds array_map(function ($s) { return $s->getId(); }, $sensors);
  1001.         $latestSms $this->getDoctrine()->getRepository(SmsLog::class)->latestBySensorIds($sensorIds);
  1002.         $rows = [];
  1003.         foreach ($sensors as $sensor) {
  1004.             $elevator $sensor->getElevator();
  1005.             $nea $sensor->getNea();
  1006.             $address null;
  1007.             if (null !== $elevator && null !== $elevator->getAddress()) {
  1008.                 $address $elevator->getAddress();
  1009.             } elseif (null !== $nea && method_exists($nea'getAddress') && null !== $nea->getAddress()) {
  1010.                 $address $nea->getAddress();
  1011.             }
  1012.             $addressStr '—';
  1013.             if (null !== $address) {
  1014.                 $addressStr trim(trim(sprintf('%s %s, %s %s'$address->getStreet(), $address->getStreetNumber(), $address->getPlz(), $address->getLocation())), ' ,') ?: '—';
  1015.             }
  1016.             $labels $sensor->getStateNames();
  1017.             $sms $latestSms[$sensor->getId()] ?? null;
  1018.             $rows[] = [
  1019.                 'id' => $sensor->getId(),
  1020.                 'serial' => $sensor->getSerial(),
  1021.                 'phone' => $sensor->getPhone(),
  1022.                 'sim_number' => $sensor->getSimNumber(),
  1023.                 'state' => $sensor->getState(),
  1024.                 'stateLabel' => $labels[$sensor->getState()] ?? (string) $sensor->getState(),
  1025.                 'address' => $addressStr,
  1026.                 'elevatorId' => null !== $elevator $elevator->getId() : null,
  1027.                 'neaId' => null !== $nea $nea->getId() : null,
  1028.                 'smsStatus' => null !== $sms $sms['status'] : null,
  1029.                 'smsLogId' => null !== $sms $sms['logId'] : null,
  1030.                 'smsCommand' => null !== $sms $sms['command'] : null,
  1031.                 'smsAt' => (null !== $sms && !empty($sms['createdAt'])) ? date('d.m.Y H:i'strtotime((string) $sms['createdAt'])) : null,
  1032.             ];
  1033.         }
  1034.         $data = ['data' => $rows'meta' => [
  1035.             'page' => $currentPage'pages' => $pages'perpage' => $limit,
  1036.             'total' => $total'sort' => $sortOrder'field' => $sortField,
  1037.         ]];
  1038.         $response = new Response(json_encode($data), 200);
  1039.         $response->headers->set('Content-Type''application/json');
  1040.         return $response;
  1041.     }
  1042.     /**
  1043.      * Gets a list of sensors.
  1044.      *
  1045.      * @Rest\Get("/getSignalAndProvider/{tgaHash}")
  1046.      *
  1047.      * @param mixed $tgaHash
  1048.      */
  1049.     public function getSignalAndProvider(Request $request$tgaHash)
  1050.     {
  1051.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1052.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1053.             $signal $this->sensorRemoteService->getSignalQuality($sensor);
  1054.             $provider $this->sensorRemoteService->getProvider($sensor);
  1055.             $response = ['signalQuality' => $this->sensorRemoteService->signalQualityToString($signal).'('.$signal.')''provider' => $provider'error' => false'errorMsg' => ''];
  1056.         } else {
  1057.             $response = ['signalQuality' => '''provider' => '''error' => true'errorMsg' => 'Sensor not found!'];
  1058.         }
  1059.         return new JsonResponse($response);
  1060.     }
  1061.     /**
  1062.      * Gets a list of sensors.
  1063.      *
  1064.      * @Rest\Get("/getSignal/{tgaHash}", name="get_signal")
  1065.      *
  1066.      * @param mixed $tgaHash
  1067.      */
  1068.     public function getSignal(Request $request$tgaHash)
  1069.     {
  1070.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1071.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1072.             $signal $this->sensorRemoteService->getSignalQuality($sensor);
  1073.             $response = ['signalQuality' => $this->sensorRemoteService->signalQualityToString($signal).'('.$signal.')''error' => false'errorMsg' => ''];
  1074.         } else {
  1075.             $response = ['signalQuality' => 'N/A''error' => true'errorMsg' => 'Sensor not found!'];
  1076.         }
  1077.         return new JsonResponse($response);
  1078.     }
  1079.     /**
  1080.      * Is sensor online.
  1081.      *
  1082.      * @Rest\Get("/isOnline/{tgaHash}", name="is_online")
  1083.      *
  1084.      * @param mixed $tgaHash
  1085.      */
  1086.     public function isOnline(Request $request$tgaHash)
  1087.     {
  1088.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1089.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1090.             $isOnline $this->sensorRemoteService->checkSSHConnection($sensor);
  1091.             // updateRunning: laeuft gerade ein Software-Update? -> InstallTool zeigt "Update laeuft"
  1092.             // statt "offline", damit der Monteur den Sensor nicht resettet. updateStatus: letzter
  1093.             // Zustand (v.a. failed_network=Empfangsprobleme), falls die Seite erst nach dem Live-
  1094.             // Event via Mercure geoeffnet/neu geladen wird.
  1095.             $response = [
  1096.                 'isOnline' => $isOnline,
  1097.                 'updateRunning' => $sensor->isUpdateRunning(),
  1098.                 'updateStatus' => $sensor->getUpdateStatus(),
  1099.                 'errorMsg' => '',
  1100.             ];
  1101.         } else {
  1102.             $response = ['isOnline' => false'updateRunning' => false'updateStatus' => null'errorMsg' => 'Sensor not found!'];
  1103.         }
  1104.         return new JsonResponse($response);
  1105.     }
  1106.     /**
  1107.      * Meldung der Update-Scripts (safe_update.py): "running" beim Start, "done"/"failed" am Ende.
  1108.      * Setzt sensor.update_started_at und pusht das InstallTool-Live-Topic.
  1109.      *
  1110.      * @Route("/sensor/updateStatus", name="sensor_update_status", methods={"PUT"})
  1111.      */
  1112.     public function updateStatusAction(Request $request)
  1113.     {
  1114.         $data json_decode($request->getContent());
  1115.         $em $this->getDoctrine()->getManager();
  1116.         $sensor = isset($data->serial)
  1117.             ? $em->getRepository(Sensor::class)->findOneBy(['serial' => $data->serial])
  1118.             : null;
  1119.         if (!$sensor) {
  1120.             return new JsonResponse(['error' => 'Sensor nicht gefunden'], 404);
  1121.         }
  1122.         $status $data->status ?? '';
  1123.         $attempt = isset($data->attempt) ? (int) $data->attempt null;
  1124.         $maxAttempts = isset($data->max_attempts) ? (int) $data->max_attempts null;
  1125.         // running/retry -> Update laeuft: Timer setzen/verlaengern (isUpdateRunning -> "laeuft").
  1126.         // done -> fertig, Feld raus. failed*/sonst -> Timer aus, aber Zustand bleibt persistent,
  1127.         // damit ein spaeter geoeffnetes InstallTool den Ausgang (v.a. Empfangsprobleme) noch zeigt.
  1128.         $running in_array($status, ['running''retry'], true);
  1129.         $sensor->setUpdateStartedAt($running ? new \DateTime() : null);
  1130.         $sensor->setUpdateStatus('done' === $status null : ($status ?: null));
  1131.         $em->flush();
  1132.         try {
  1133.             $this->hub->publish(new Update(
  1134.                 'https://acobesmart.com/sensorinstall_'.$this->appKernel->getEnvironment().'_'.$sensor->getId(),
  1135.                 json_encode(['type' => 'update''status' => $status'attempt' => $attempt'max' => $maxAttempts])
  1136.             ));
  1137.         } catch (\Throwable $e) {
  1138.             $this->logger->error('updateStatus mercure: '.$e->getMessage());
  1139.         }
  1140.         return new JsonResponse(['ok' => 'ok']);
  1141.     }
  1142.     /**
  1143.      * Is sensor online.
  1144.      *
  1145.      * @Rest\Get("/clearSensorDatabase/{tgaHash}", name="clear_sensor_database")
  1146.      *
  1147.      * @param mixed $tgaHash
  1148.      */
  1149.     public function clearSensorDatabase(Request $request$tgaHash)
  1150.     {
  1151.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1152.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1153.             $cleared $this->sensorRemoteService->clearSensorDatabase($sensor);
  1154.             $this->sensorRemoteService->restartAllScripts($sensor);
  1155.             $response = ['cleared' => $cleared'errorMsg' => ''];
  1156.         } else {
  1157.             $response = ['cleared' => false'errorMsg' => 'Error on clearing sensor DB.'];
  1158.         }
  1159.         return new JsonResponse($response);
  1160.     }
  1161.     /**
  1162.      * Gets a list of sensors.
  1163.      *
  1164.      * @Rest\Get("/getProvider/{tgaHash}", name="get_provider")
  1165.      *
  1166.      * @param mixed $tgaHash
  1167.      */
  1168.     public function getProvider(Request $request$tgaHash)
  1169.     {
  1170.         $repository $this->getDoctrine()->getRepository(Sensor::class);
  1171.         if ($sensor $repository->findOneByTgaHash($tgaHash)) {
  1172.             $provider $this->sensorRemoteService->getProvider($sensor);
  1173.             $response = ['provider' => $provider'error' => false'errorMsg' => ''];
  1174.         } else {
  1175.             $response = ['provider' => 'N/A''error' => true'errorMsg' => 'Sensor not found!'];
  1176.         }
  1177.         return new JsonResponse($response);
  1178.     }
  1179.     private function serializeSensors(Sensor $sensor)
  1180.     {
  1181.         $serialized = [
  1182.             'id' => $sensor->getId(),
  1183.             'sensorId' => $sensor->getId(),
  1184.             'createdAt' => $sensor->getCreatedAt()->format('d.m.Y H:i:s'),
  1185.             'stateName' => $sensor->getStateName(),
  1186.             'typeOfState' => $sensor->getTypeOfState(),
  1187.             'lastOnline' => ($sensor->getLastOnline() ? $sensor->getLastOnline()->format('d.m.Y H:i:s') : 'N/A'),
  1188.             'observerState' => $sensor->getObserverStateName(),
  1189.             'typeOfObserverState' => $sensor->getTypeOfObserverState(),
  1190.             'observerLastOnline' => ($sensor->getObserverLastOnline() ? $sensor->getObserverLastOnline()->format('d.m.Y H:i:s') : 'N/A'),
  1191.             'observerVersion' => $sensor->getObserverVersion(),
  1192.             'serial' => $sensor->getSerial(),
  1193.             'port' => $sensor->getPort(),
  1194.         ];
  1195.         $actionArray = [];
  1196.         $actionArray['showSensorqrCode'] = $this->generateUrl('show_sensor_qr_coed', ['sensorHash' => $sensor->getTgaHash()]);
  1197.         $serialized['actions'] = $actionArray;
  1198.         return $serialized;
  1199.     }
  1200. }