Raspberry Pi hosted web application to manage Energenie sockets II

Raspberry Pi hosted web application to manage Energenie sockets II

In the previous post, I explained how to set up and deploy a simple web application to be able to use a Raspberry Pi and the Energenie PiMote control board to switch the remote controlled Energenie sockets I purchased several years ago. In this second part, we will develop the application a bit further to create the necessary software to program our system to switch our lights at certain times and therefore create the appearance of presence at home when we are away.

The domain

We need to include a new element in our domain to represent when a socket will be programmed to perform one of the operations (ON, or OFF). This element will be described in the data model as an Event. In it, we need to persist the Socket that will be actioned by the event, the type of operation and the time when the operation will be triggered.

<?php

namespace App\Domain;

use App\Domain\Socket;
use Doctrine\ORM\Mapping as ORM;

/**
 * Event
 *
 * @ORM\Table(name="homedomo.events")
 * @ORM\Entity
 */
class Event
{
	/**
	 * @var integer
	 *
	 * @ORM\Column(name="id", type="integer", nullable=false)
	 * @ORM\Id
	 * @ORM\GeneratedValue(strategy="IDENTITY")
	 */
	protected $id;

    /**
     * @var Socket
     * 
     * @ORM\ManyToOne(targetEntity="Socket", inversedBy="events")
     * @ORM\JoinColumn(name="socket_id", referencedColumnName="id")
     */
    protected $socket;

	/**
	 * @var string
	 *
	 * @ORM\Column(name="socket_operation", type="string", length=10, nullable=false)
	 */
	protected $operationName;

	/**
	 * @var integer
	 *
	 * @ORM\Column(name="time", type="integer", nullable=false)
	 */
	protected $time;

	/**
	 * Get the value of id
	 *
	 * @return integer
	 */ 
	public function getId()
	{
		return $this->id;
	}

    /**
     * Get the value of socket
     *
     * @return Socket
     */ 
    public function getSocket()
    {
        return $this->socket;
    }

    /**
     * Set the value of socket
     *
     * @param Socket $socket
     *
     * @return self
     */ 
    public function setSocket(Socket $socket)
    {
        $this->socket = $socket;

        return $this;
    }

	/**
	 * Get the value of operationName
	 *
	 * @return string
	 */ 
	public function getOperationName()
	{
		return $this->operationName;
	}

	/**
	 * Set the value of operationName
	 *
	 * @param string $operationName
	 *
	 * @return self
	 */ 
	public function setOperationName(string $operationName)
	{
		$this->operationName = $operationName;

		return $this;
	}

	/**
	 * Get the value of time
	 *
	 * @return integer
	 */ 
	public function getTime()
	{
		return $this->time;
	}

	/**
	 * Set the value of time
	 *
	 * @param integer $time
	 *
	 * @return self
	 */ 
	public function setTime($time)
	{
		$this->time = $time;

		return $this;
	}

	public function getHours()
    {
        return intval($this->time / 100);
    }

    public function getMinutes()
    {
        return $this->time % 100;
	}
	
	public function getTimeString()
    {
		$hoursString = substr("00{$this->getHours()}", -2, 2);
		$minutesString = substr("00{$this->getMinutes()}", -2, 2);
        return "{$hoursString}:{$minutesString}";
	}

	public function toArray()
	{
		return [
			"id" => $this->id,
			"socket" => [
				"id" => $this->socket->getId(),
				"name" => $this->socket->getName(),
				"description" => $this->socket->getDescription()
			],
			"operationName" => $this->getOperationName(),
			"time" => $this->getTime(),
			"timeString" => $this->getTimeString()
		];
	}

	public function toJson()
	{
		return json_encode($this->toArray());
	}
}
Event.php

As we can see, the events table on DB will be linked to the specific Socket through a foreign key and will also have a string field to store the operation name and an integer value to represent the time of the day from 0 (00:00) to 2359 (11:59 PM). To create this table on our DB, we can just run php vendor/bin/doctrine orm:schema-tool:update. Please note that this is a way to update the DB schema while developing. To update the DB at the deployment stage, we should take advantage of doctrine migrations, which I have left out of these articles for the sake of simplicity.

We should now create the inverse relationship on Socket:

<?php

namespace App\Domain;

use App\Domain\Event;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use DateTime;

/**
 * Socket
 *
 * @ORM\Table(name="homedomo.sockets")
 * @ORM\Entity
 */
class Socket
{
	// ...

    /**
     * @var ArrayCollection
     * 
     * @ORM\OneToMany(targetEntity="Event", mappedBy="socket")
	 * @ORM\OrderBy({"time" = "ASC"})
     */
	protected $events;

	// ...
    
    /**
     * Add Event
     *
     * @return Socket
     */
    public function addEvent(Event $event)
    {
        $this->events->add[] = $event;

        return $this;
    }

    /**
     * Remove Event
     */
    public function removeEvent(Event $event)
    {
        $this->events->removeElement($event);
        
        return $this;
    }

    /**
     * Get the value of events
     *
     * @return ArrayCollection
     */ 
    public function getEvents()
    {
        return $this->events;
	}

	public function getNextEvent($time = null)
	{
		$time = $time ?? intval((new DateTime())->format('Hi'));
		$nextEvents = $this->getEvents()->filter(function(Event $event) use ($time){
						return $event->getTime() > $time;
					});

		return $nextEvents->first() ?: $this->events->first();
	}
	
	public function getPreviousEvent($time = null)
	{
		$time = $time ?? intval((new DateTime())->format('Hi'));
		$prevEvents = $this->getEvents()->filter(function(Event $event) use ($time){
						return $event->getTime() < $time;
					});
		return $prevEvents->last() ?: $this->events->last();
	}
	
	public function toArray()
	{
		return [
			"id" => $this->getId(),
			"name" => $this->getName(),
			"description" => $this->getDescription(),
			"events" => array_map(function($event){
				return [
					"id" => $event->getId(),
					"operationName" => $event->getOperationName(),
					"time" => $event->getTime(),
					"timeString" => $event->getTimeString()
				];
			}, $this->events->toArray()),
			"nextEvent" => $this->getNextEvent()->toArray(),
			"previousEvent" => $this->getPreviousEvent()->toArray(),
		];
	}

	public function toJson()
	{
		return json_encode($this->toArray());
	}
}
Sockets.php

And lastly, develop a simple service to retrieve our Events when necessary:

<?php

namespace App\Domain;

use Doctrine\ORM\EntityManagerInterface;
use App\Domain\Event;
use App\Domain\Socket;
use DateTime;

final class EventService
{
    protected $em;
    protected $eventRepository;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
        $this->eventRepository = $em->getRepository(Event::class);
    }

    public function findAll()
    {
        return $this->eventRepository->findAll();
    }

    public function find($id)
    {
        return $this->eventRepository->find($id);
    }

    public function findDue($time = null)
    {
        $time = $time ?? intval((new DateTime())->format('Hi'));

        return $this->eventRepository->findBy(['time' => $time]);
    }
}
EventService.php

Running the Schedule

To be able to execute events, we're going to make use of Symfony console commands, which use the console component.

 composer require symfony/console

First, we need to create a PHP script to define de console application:

<?php
// application.php

require __DIR__.'/../vendor/autoload.php';

use Symfony\Component\Console\Application;

$application = new Application();

/** @var ContainerInterface $container */
$container = (require __DIR__ . '/../config/bootstrap.php')->getContainer();
$commands = $container->get('commands');

$application = new Application();

foreach ($commands as $class) {
    $application->add($container->get($class));
}

$application->run();
bin/console.php

And for this to work create the cli-config.php file in the root of the project:

<?php

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\ConsoleRunner;

$container = (require __DIR__ . '/config/bootstrap.php')->getContainer();

return ConsoleRunner::createHelperSet($container->get(EntityManagerInterface::class));
cli-config.php

Having these, we can create a symfony that will run our scheduled events. For more information on Symfony commands, visit the Symfony docs:

<?php

namespace App\Application\Commands;

use App\Application\SocketOperations\SocketOperationFactory;
use App\Domain\EventService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ScheduleRunCommand extends Command
{
    protected $eventService;
    protected $socketOperationFactory;

    public function __construct(EventService $eventService, SocketOperationFactory $socketOperationFactory)
    {
        $this->eventService = $eventService;
        $this->socketOperationFactory = $socketOperationFactory;
        parent::__construct();
    }

    protected function configure(): void
    {
        parent::configure();

        $this->setName('schedule:run');
        $this->setDescription('Run the scheduled events');
    }

    protected function getOperation($operationName, $socket)
    {
        return $this->socketOperationFactory->createSocketOperation($operationName, $socket);
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $events = $this->eventService->findDue();

        if ($events)
        {
            $output->writeln("Running events");

            foreach ($events as $event)
            {
                $operation = $this->getOperation($event->getOperationName(), $event->getSocket());
                $operation->run();
            }

        }

        return 0;
    }
}
ScheduleRunCommand.php

If we now execute the following in our evironment:

php /var/www/html/bin/console.php schedule:run

The events (if any) that are due this very minute, will be executed.

And finally, we register our ScheduleRunCommand on our bootstrap file:

?php

use App\Application\Commands\ScheduleRunCommand;
use App\Controllers\EventController;
use App\Controllers\HomeController;
use App\Controllers\SocketController;
use App\Controllers\SwitchController;
use App\Utility\Configuration;
use DI\ContainerBuilder;

// ...

    // Twig templates
    Twig::class => function () {
        return Twig::create(__DIR__ . '/../templates', ['cache' => false]);
    },

    // Register commands
    'commands' => [
        ScheduleRunCommand::class
    ],

    // Configuration
    Configuration::class => new Configuration([
        // python scripts path
        'scripts_path' => __DIR__ . '/../bin/scripts'
    ])
    
// ...
    
bootstrap.php

In order for this to work, we need to create a cron job, to execute our ScheduleRunCommand every minute, the line

$events = $this->eventService->findDue();

will make sure only the due events are executed at the scheduled time. We will come back to this later on this article, at the deployment stage.

Controllers

Now we need to create controllers for the event creations. As in the previous article, we choose to use a class method per controller action so we will create a class to develop all REST actions for Events.


<?php

namespace App\Controllers;

use App\Domain\Event;
use App\Domain\Socket;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;

class EventController
{
    protected $em;
    protected $view;

    public function __construct(EntityManagerInterface $em, Twig $view) {
        $this->em = $em;
        $this->view = $view;
    }

    public function list(Request $request, Response $response, $args)
    {
        $allEvents = $this->em->getRepository(Event::class)->findAll();
        $events = array_map(function($event){ return $event->toArray();}, $allEvents);
        $response->getBody()->write(json_encode($events));
        return $response->withHeader("Content-Type", "application/json");
    }

    public function get(Request $request, Response $response, $args)
    {
        $event = $this->em->getRepository(Event::class)->find($args["id"]);
        $response->getBody()->write($event->toJson());
        
        return $response->withHeader("Content-Type", "application/json");
    }

    public function store(Request $request, Response $response, $args)
    {
        $data = $request->getParsedBody();
        $socket = $this->em->getRepository(Socket::class)->find($data["socket_id"]);
        $operationName = $data["operation_name"];
        $time = $data["time"];

        // TODO validate

        $event = new Event();
        $event->setSocket($socket)
            ->setOperationName($operationName)
            ->setTime($time);        
        $socket->addEvent($event);
        $this->em->persist($event);
        $this->em->persist($socket);
        $this->em->flush();
        $response->getBody()->write($event->toJson());

        return $response->withHeader("Content-Type", "application/json");
    }

    public function update(Request $request, Response $response, $args)
    {
        $data = $request->getParsedBody();
        $eventId = $args["id"];
        $operationName = $data["operation_name"];
        $time = $data["time"];

        // TODO validate

        $event = $this->em->getRepository(Event::class)->find($eventId);
        $event->setOperationName($operationName)->setTime($time);        
        $this->em->persist($event);
        $this->em->flush();
        $response->getBody()->write($event->toJson());

        return $response->withHeader("Content-Type", "application/json");
    }

    public function delete(Request $request, Response $response, $args)
    {
        $eventId = $args["id"];

        // TODO validate

        $event = $this->em->getRepository(Event::class)->find($eventId);
        $this->em->remove($event);
        $this->em->flush();
        $response->getBody()->write(json_encode(["message" => "event successfully deleted"]));

        return $response->withHeader("Content-Type", "application/json");
    }

    public function edit(Request $request, Response $response, $args)
    {
        $eventId = $args["id"] ?? false;
        // TODO validate
        $event = $eventId ? $this->em->getRepository(Event::class)->find($eventId) : null;
        
        return $this->view->render($response, "forms/eventForm.twig", ['event' => $event]);
    }
}
EventController.php

And add the routes to our bootstrap file:

// ...

// Add middleware
$app->addBodyParsingMiddleware();
$app->add(TwigMiddleware::createFromContainer($app, Twig::class));

// Register routes
$app->post('/switch/{power}', SwitchController::class . ':switch');
$app->get('/', HomeController::class . ':home');

// Events
$app->get('/events/edit[/{id}]', EventController::class . ':edit');
$app->get('/events', EventController::class . ':list');
$app->get('/events/{id}', EventController::class . ':get');
$app->post('/events', EventController::class . ':store');
$app->put('/events/{id}', EventController::class . ':update');
$app->delete('/events/{id}', EventController::class . ':delete');

//...
bootstrap.php

As in the previous article we didn't develop the functionality to edit our socket info, we will now also create the Socket controller:

<?php

namespace App\Controllers;

use App\Domain\Socket;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;

class SocketController
{
    protected $em;
    protected $view;

    public function __construct(EntityManagerInterface $em, Twig $view) {
        $this->em = $em;
        $this->view = $view;
    }

    public function list(Request $request, Response $response, $args)
    {
        $allSockets = $this->em->getRepository(Socket::class)->findAll();
        $sockets = array_map(function($socket){ return $socket->toArray();}, $allSockets);
        $response->getBody()->write(json_encode($sockets));
        return $response->withHeader("Content-Type", "application/json");
    }

    public function get(Request $request, Response $response, $args)
    {
        $socket = $this->em->getRepository(Socket::class)->find($args["id"]);
        $response->getBody()->write($socket->toJson());
        
        return $response->withHeader("Content-Type", "application/json");
    }

    public function store(Request $request, Response $response, $args)
    {
        $data = $request->getParsedBody();
        $name = $data["name"];
        $description = $data["description"];

        // TODO validate

        $socket = new Socket();
        $socket->setName($name)->setDescription($description);
        $this->em->persist($socket);
        $this->em->flush();
        $response->getBody()->write($socket->toJson());

        return $response->withHeader("Content-Type", "application/json");
    }

    public function update(Request $request, Response $response, $args)
    {
        $data = $request->getParsedBody();
        $socketId = $args["id"];
        $name = $data["name"];
        $description = $data["description"];

        // TODO validate

        $socket = $this->em->getRepository(Socket::class)->find($socketId);
        $socket->setName($name)->setDescription($description);
        $this->em->persist($socket);
        $this->em->flush();
        $response->getBody()->write($socket->toJson());

        return $response->withHeader("Content-Type", "application/json");
    }

    public function delete(Request $request, Response $response, $args)
    {
        $socketId = $args["id"];

        // TODO 
        // validate
        // delete all linked events

        $socket = $this->em->getRepository(Socket::class)->find($socketId);
        $this->em->remove($socket);
        $this->em->flush();
        $response->getBody()->write(json_encode(["message" => "socket successfully deleted"]));

        return $response->withHeader("Content-Type", "application/json");
    }

    public function edit(Request $request, Response $response, $args)
    {        
        $socketId = $args["id"] ?? false;
        // TODO validate
        $socket = $socketId ? $this->em->getRepository(Socket::class)->find($socketId) : null;
        
        return $this->view->render($response, "forms/socketForm.twig", ['socket' => $socket]);
    }

    public function editEvents(Request $request, Response $response, $args)
    {        
        $socketId = $args["id"] ?? false;
        // TODO validate
        $socket = $socketId ? $this->em->getRepository(Socket::class)->find($socketId) : null;
        
        return $this->view->render($response, "forms/eventEditForm.twig", ['socket' => $socket]);
    }

    
}

and add its routes to bootstrap.php

// ...

// Events

// ...

// Socket
$app->get('/sockets/edit[/{id}]', SocketController::class . ':edit');
$app->get('/sockets/{id}/edit-events', SocketController::class . ':editEvents');
$app->get('/sockets', SocketController::class . ':list');
$app->get('/sockets/{id}', SocketController::class . ':get');
$app->post('/sockets', SocketController::class . ':store');
$app->put('/sockets/{id}', SocketController::class . ':update');
$app->delete('/sockets/{id}', SocketController::class . ':delete');

return $app;

View

The last modification of our software is to edit the view to accomodate the new functionality.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <link rel="icon" href="/docs/4.0/assets/img/favicons/favicon.ico">

        <title>Cam House sockets</title>

        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
        <style>

            html,
            body {
                height: 100%;
            }
            label {
                font-size:87%
            }
            .socket-row {
                
            }

            .timeline {
                list-style-type: none;
                position: relative;
            }
            /* line */
            .timeline:before {
                content: ' ';
                background: #d4d9df;
                display: inline-block;
                position: absolute;
                left: 19px;
                width: 2px;
                height: 100%;
                z-index: 400;
            }

            .timeline-element {
                margin: 5px 0;
                padding: 10px 15px 10px 40px;
            }

            .timeline-element:hover {
                background-color: #f8f9fa;
            }

            /* timeline icon */
            .timeline-element:before {
                content: ' ';
                background: white;
                display: inline-block;
                position: absolute;
                border-radius: 50%;
                border: 3px solid #d4d9df;
                left: 10px;
                width: 20px;
                height: 20px;
                z-index: 400;
            }

            .timeline-element.dot-danger:before {
                border-color: #dc3545;
            }

            .timeline-element.dot-success:before {
                border-color: #28a745;
            }

            .timeline-element.dot-info:before {
                border-color: #17a2b8;
            }

            .timeline-element.dot-warning:before {
                border-color: #ffc107
            }

            .socket-detail-row .socket-detail-card-body-wrapper {
                height: 150px;
                overflow: auto;
            }

            .socket-detail-card-header {
                padding: .50rem 1rem;
            }

        </style>
    </head>

    <body>
        <div class="container d-flex h-100 p-3 mx-auto flex-column">
            <header class="mb-auto">
                <h3 class="brand">Cam House Sockets</h3>
            </header>

            <main role="main">
                <div class="alert" role="alert" style="display:none">
                </div>
                {% for socket in sockets %}
                    <div data-id={{ socket.id }} data-name="{{ socket.name }}" class="socket-row">
                        <div class="row border-top pb-1 pt-1">
                            <div class="col-sm-1 align-self-center"># {{ socket.id }}</div>
                            <div class="col-sm align-self-center">{{ socket.name }}</div>
                            <div class="col-sm align-self-center">{{ socket.description }}</div>
                            <div class="col-sm align-self-center">
                                <div class="btn-group" role="group">
                                    <button class="btn btn-warning btn-on">on</button> 
                                    <button class="btn btn-secondary btn-off">off</button> 
                                </div>
                                <button class="btn btn-outline-dark" data-toggle="collapse" href="#socketDetail{{ socket.id }}" role="button" aria-expanded="false" aria-controls="socketDetail{{ socket.id }}">
                                    <i class="fa fa-caret-down"></i>
                                </button>
                            </div>
                        </div>
                        <div id="socketDetail{{ socket.id }}" class="socket-detail-row row border-top pb-1 pt-1 collapse">
                            <div class="col-sm align-self-center">
                                <div class="card socket-detail-card">
                                    <div class="card-header socket-detail-card-header">
                                        <i class="far fa-clock"></i> Programmed actions
                                        <span class="float-right">
                                            <button class="btn btn-info btn-sm btn-new-event"><i class="fas fa-plus"></i> Add action</button>
                                            <button class="btn btn-info btn-sm btn-edit-events"><i class="fas fa-edit"></i> Edit events</button>
                                        </span>
                                    </div>
                                    <div class="socket-detail-card-body-wrapper">
                                        <div class="card-body">
                                            {% if socket.events is not empty %}
                                                <div class="timeline">
                                                    {% for event in socket.events %}
                                                        <div data-event-id={{event.id}} class="timeline-element {% if event.operationName == 'on' %}dot-warning{% endif %}">
                                                            <div>
                                                                <span>switch <span class="badge {% if event.operationName == 'on' %}badge-warning{% else %}badge-secondary{% endif %}">{{ event.operationName }}</span>, at {{ event.timeString }}</span>
                                                                <div class="float-right">
                                                                    <button class="btn btn-outline-secondary btn-sm btn-edit-event" title="edit"><i class="fas fa-edit"></i></button>
                                                                    <button class="btn btn-outline-secondary btn-sm btn-delete-event" title="delete"><i class="fas fa-trash"></i></button>
                                                                </div>
                                                            </div>
                                                        </div>
                                                    {% endfor %}
                                                </div>
                                            {% else %}
                                                <div>No actions programmed for this socket</div>
                                            {% endif %}
                                        </div>
                                    </div>
                                </div>
                            </div>
                            <div class="col-sm align-self-center">
                                <div class="card socket-detail-card">
                                    <div class="card-header socket-detail-card-header">
                                        <span class="">
                                            <i class="fas fa-cog"></i> Socket
                                        </span>
                                        <span class="float-right">
                                            <button class="btn btn-info btn-sm btn-pair"><i class="fas fa-link"></i> Pair</button> <button class="btn btn-success btn-sm btn-edit-socket"><i class="fas fa-edit"></i> Edit</button>
                                        </span>
                                    </div>
                                    <div class="socket-detail-card-body-wrapper">
                                        <ul class="list-group list-group-flush">
                                            {% if socket.events is not empty %}
                                            <li class="list-group-item">
                                                This socket should be <span class="badge {% if socket.previousEvent.operationName == 'on' %}badge-warning{% else %}badge-secondary{% endif %}">{{ socket.previousEvent.operationName }}</span> since {{socket.previousEvent.timeString}}
                                            </li>
                                            <li class="list-group-item">
                                                Next action: switch <span class="badge {% if socket.nextEvent.operationName == 'on' %}badge-warning{% else %}badge-secondary{% endif %}">{{ socket.nextEvent.operationName }}</span> at {{socket.nextEvent.timeString}}
                                            </li>
                                            {% else %}
                                                <div class="card-body">
                                                    <div>No next or previous actions to show</div>
                                                </div>
                                            {% endif %}
                                        </ul>
                                    </div>
                                </div>
                            
                            </div>
                        </div>
                    </div>
                {% endfor %}
            </main>

            <footer class="mt-auto">
            </footer>
        </div>

        <div class="modal fade" id="formModal" tabindex="-1" role="dialog" aria-labelledby="formModalLabel" aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="formModalLabel"></h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary modal-action-btn">Submit</button>
                </div>
                </div>
            </div>
        </div>
        <!-- JS, Popper.js, and jQuery -->
        <script src="https://code.jquery.com/jquery-3.5.1.js" integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
    
        <script>
        	$(document).ready(function() {

                function serializeFormJson(form) {
                    var output = {};
                    var serialized = form.serializeArray();
                    $.each(serialized, function () {
                        if (output[this.name]) {
                            if (!output[this.name].push) {
                                output[this.name] = [output[this.name]];
                            }
                            output[this.name].push(this.value || '');
                        } else {
                            output[this.name] = this.value || '';
                        }
                    });
                    return output;
                };


                function switchSocket(url, socket){
                    $.post(url, { "socket" : socket }, null, "json")
                    .done((data) => {
                        $alert = $('.alert')
                        $alert.addClass('alert-success').html(data.message).fadeIn()
                        setTimeout(() => $alert.fadeOut(), 2000)
                    })
                    .fail((data) => {
                        $alert = $('.alert')
                        $alert.addClass('alert-danger').html(data.responseJSON.message).fadeIn()
                        setTimeout(() => $alert.fadeOut(), 2000)
                    });
                }

                $(".btn-on").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    switchSocket("/switch/on", socketId)
                });

                $(".btn-off").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    switchSocket("/switch/off", socketId)
                });

                $(".btn-pair").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    switchSocket("/switch/pair", socketId)
                });

                $(".btn-new-event").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    const socketName = $(this).closest(".socket-row").data("name")
                    editEvent(false, socketId)
                });

                $(".btn-edit-event").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    const eventId = $(this).closest(".timeline-element").data("event-id")
                    editEvent(eventId, socketId)
                });

                $(".btn-edit-events").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    editEvents(socketId)
                });

                $(".btn-delete-event").click(function() {
                    const eventId = $(this).closest(".timeline-element").data("event-id")
                    const confirmed = confirm("Are you sure?");
                    if (confirmed)
                    {
                        $.ajax({
                            url: `/events/${eventId}`,
                            method: "DELETE"
                        })
                        .done(() => {
                            location.reload();
                        })
                    }
                });

                function editEvent(id, socketId) {
                    const url = "/events/edit" + (id ? `/${id}` : "")
                    const modal = $("#formModal")
                    const title = (id ? "Edit event" : "Add new event") + ` for socket ${socketId}`;
                    $.get(url)
                    .done((htmlForm) => {
                        $(".modal-title").html(title)
                        $(".modal-body").html(htmlForm)
                        $(".modal-action-btn").click(() => {
                            const form = $("#event-form")
                            const jsonFormData = serializeFormJson(form);
                            const data = {
                                socket_id: socketId,
                                operation_name: jsonFormData.operation_name,
                                time: jsonFormData.hours + jsonFormData.minutes
                            }
                            $.ajax({
                                url: "/events" + (id ? `/${id}` : ""),
                                method: id ? "PUT" : "POST",
                                data:data
                            })
                            .done(() => {
                                location.reload();
                            })
                            
                        })
                        modal.modal({show: true})
                    });
                }

                function editEvents(socketId) {
                    const url = `/sockets/${socketId}/edit-events`
                    const modal = $("#formModal")
                    const title = "Edit events"
                    $.get(url)
                    .done((htmlForm) => {
                        $(".modal-title").html(title)
                        $(".modal-body").html(htmlForm)
                        $(".modal-action-btn").click(() => {
                            const form = $("#event-form")
                            const jsonFormData = serializeFormJson(form);
                            const data = {
                                socket_id: socketId,
                                operation_name: jsonFormData.operation_name,
                                time: jsonFormData.hours + jsonFormData.minutes
                            }
                            {# $.ajax({
                                url: "/events" + (id ? `/${id}` : ""),
                                method: id ? "PUT" : "POST",
                                data:data
                            })
                            .done(() => {
                                location.reload();
                            }) #}
                            
                        })
                        modal.modal({show: true})
                    });
                }

                $(".btn-edit-socket").click(function() {
                    const socketId = $(this).closest(".socket-row").data("id")
                    editSocket(socketId)
                });

                function editSocket(socketId) {
                    const url = "/sockets/edit" + (socketId ? `/${socketId}` : "")
                    const modal = $("#formModal")
                    const title = (socketId ? `Edit socket ${socketId}` : "Add new socket");
                    $.get(url)
                    .done((htmlForm) => {
                        $(".modal-title").html(title)
                        $(".modal-body").html(htmlForm)
                        $(".modal-action-btn").click(() => {
                            const form = $("#socket-form")
                            const jsonFormData = serializeFormJson(form);
                            $.ajax({
                                url: "/sockets" + (socketId ? `/${socketId}` : ""),
                                method: socketId ? "PUT" : "POST",
                                data:jsonFormData
                            })
                            .done(() => {
                                location.reload();
                            })
                            
                        })
                        modal.modal({show: true})
                    });
                }

            });
        </script>
    </body>
</html>
index.twig

And views to display the forms to create and edit events:

<form id="event-form" class="form-row">
    <div class="col form-group">
        <select name="operation_name" class="form-control">{{ event.operationName }}
        {% for operation in ['on', 'off'] %}
            <option {{ (event.operationName and event.operationName == operation) ? 'selected' : '' }}>{{ operation }}</option>
        {% endfor %}
        </select>
    </div>
    <div class="col form-group">
        <select name="hours" class="form-control">
        {% for i in range(0, 23) %}
            <option {{ (event.time and ('0' ~ event.time | slice(-4, 2)) == i) ? 'selected' : '' }}>{{ i < 10 ? '0' : ''}}{{i}}</option>
        {% endfor %}
        </select>
    </div>:
    <div class="col form-group">
        <select name="minutes" class="form-control">
        {% for i in range(0, 59) %}
            <option {{ (event.time and ('0' ~ event.time | slice(-2, 2)) == i) ? 'selected' : '' }}>{{ i < 10 ? '0' : ''}}{{i}}</option>
        {% endfor %}
        </select>
    </div>
</form>
create event, eventForm.twig
<form id="events-form">
    <div class="form-rows">
    {% for event in socket.events %}
        <div class="form-row">
            <div class="col form-group">
                <input type="hidden" name="id" value="{{ event.id }}">
                <select name="operation_name" class="form-control">
                {% for operation in ["on", "off"] %}
                    <option {{ (event.operationName and event.operationName == operation) ? "selected" : "" }}>{{ operation }}</option>
                {% endfor %}
                </select>
            </div>
            <div class="col form-group">
                <select name="hours" class="form-control">
                {% for i in range(0, 23) %}
                    <option {{ (event.time and (("000" ~ event.time) | slice(-4, 2)) == i) ? "selected" : "" }}>{{ i < 10 ? "0" : ""}}{{i}}</option>
                {% endfor %}
                </select>
            </div>:
            <div class="col form-group">
                <select name="minutes" class="form-control">
                {% for i in range(0, 59) %}
                    <option {{ (event.time and (("0" ~ event.time) | slice(-2, 2)) == i) ? "selected" : "" }}>{{ i < 10 ? "0" : ""}}{{i}}</option>
                {% endfor %}
                </select>
            </div>
            <div class="col-1">
                <button class="btn btn-danger btn-delete-event"><i class="fas fa-trash"></i></button>
            </div>
        </div>
    {% endfor %}
    </div>
    <div class="col">
        <div class="form-row">
            <button class="btn btn-info btn-sm btn-add-event"><i class="fas fa-plus"></i> Add event</button>
        </div>
    </div>

        <script>
        	$(document).ready(function() {

                $(".btn-add-event").click(function(e) {
                    e.preventDefault();

                    var formRow =                    
                        `<div class="form-row">
                            <div class="col form-group">
                                <input type="hidden" name="id" value="{{ event.id }}">
                                <select name="operation_name" class="form-control">
                                {% for operation in ["on", "off"] %}
                                    <option>{{ operation }}</option>
                                {% endfor %}
                                </select>
                            </div>
                            <div class="col form-group">
                                <select name="hours" class="form-control">
                                {% for i in range(0, 23) %}
                                    <option>{{ i < 10 ? "0" : ""}}{{i}}</option>
                                {% endfor %}
                                </select>
                            </div>:
                            <div class="col form-group">
                                <select name="minutes" class="form-control">
                                {% for i in range(0, 59) %}
                                    <option>{{ i < 10 ? "0" : ""}}{{i}}</option>
                                {% endfor %}
                                </select>
                            </div>
                            <div class="col-1">
                                <button class="btn btn-danger btn-delete-event"><i class="fas fa-trash"></i></button>
                            </div>
                        </div>`;

                        $(".form-rows").append(formRow);
                });

                $(document).on("click", ".btn-delete-event", function(e) {
                    e.preventDefault();
                    $(this).closest(".form-row").remove();
                });

            });
        </script>
</form>
edit event, eventsEditForm.twig

Finaly, the view to edit the socket info:

<form id="socket-form">
    <div class="row">
        <div class="col form-group">
        <label for="name">Name</label>
            <input name="name" class="form-control" value="{{ socket ? socket.name : '' }}">
        </div>
    </div>
    <div class="row">
        <div class="col form-group">
        <label for="description">Description</label>
            <input name="description" class="form-control" value="{{ socket ? socket.description : '' }}">
        </div>
    </div>
</form>
socketForm.twig

Our web application has now been modified to accomodate all the new scheduling events and editing information:

Home page
Edit events
Edit socket

Deployment

As in the previous article, we will not set up any complex deployment strategy, so I'd refer to the previous article to see how to upload files and add permissions.

As mentioned earlier, once the code is deployed we need to make use of a cron job to execute our command. To edit the cron jobs on our deployment server, we type.

crontab -e

And then add the following line to execute the ScheduleRunCommand every minute:

* * * * * /usr/bin/php /var/www/html/bin/console.php schedule:run

Conclusion

With this second development, we have updated the software to complete our project. We can now schedule our sockets to switch our lights at different times and give the appearance of the house being inhabited even if it is not.

Show Comments