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">×</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.