diff --git a/db/migrations/20240408121908_display_alerts_migration.php b/db/migrations/20240408121908_display_alerts_migration.php
new file mode 100644
index 0000000000..29071a4208
--- /dev/null
+++ b/db/migrations/20240408121908_display_alerts_migration.php
@@ -0,0 +1,50 @@
+.
+ */
+
+use Phinx\Migration\AbstractMigration;
+
+/**
+ * Migration for adding more columns to displayevent table.
+ * Add a new column on Command table for createAlertOn.
+ * Add a new column on lkcommanddisplayprofile for createAlertOn.
+ * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
+ */
+class DisplayAlertsMigration extends AbstractMigration
+{
+ public function change(): void
+ {
+ $this->table('displayevent')
+ ->changeColumn('start', 'integer', ['null' => true])
+ ->addColumn('eventTypeId', 'integer', ['null' => false, 'default' => 1])
+ ->addColumn('refId', 'integer', ['null' => true, 'default' => null])
+ ->addColumn('detail', 'text', ['null' => true, 'default' => null])
+ ->save();
+
+ $this->table('command')
+ ->addColumn('createAlertOn', 'string', ['null' => false, 'default' => 'never'])
+ ->save();
+
+ $this->table('lkcommanddisplayprofile')
+ ->addColumn('createAlertOn', 'string', ['null' => true, 'default' => null])
+ ->save();
+ }
+}
diff --git a/lib/Controller/Command.php b/lib/Controller/Command.php
index b2d94d4e5d..a6847ed506 100644
--- a/lib/Controller/Command.php
+++ b/lib/Controller/Command.php
@@ -369,6 +369,14 @@ public function deleteForm(Request $request, Response $response, $id)
* type="string",
* required=false
* ),
+ * @SWG\Parameter(
+ * name="createAlertOn",
+ * in="formData",
+ * description="On command execution, when should a Display alert be created?
+ * success, failure, always or never",
+ * type="string",
+ * required=false
+ * ),
* @SWG\Response(
* response=201,
* description="successful operation",
@@ -398,6 +406,7 @@ public function add(Request $request, Response $response)
$command->userId = $this->getUser()->userId;
$command->commandString = $sanitizedParams->getString('commandString');
$command->validationString = $sanitizedParams->getString('validationString');
+ $command->createAlertOn = $sanitizedParams->getString('createAlertOn', ['default' => 'never']);
$availableOn = $sanitizedParams->getArray('availableOn');
if (empty($availableOn)) {
$command->availableOn = null;
@@ -476,6 +485,14 @@ public function add(Request $request, Response $response)
* type="string",
* required=false
* ),
+ * @SWG\Parameter(
+ * name="createAlertOn",
+ * in="formData",
+ * description="On command execution, when should a Display alert be created?
+ * success, failure, always or never",
+ * type="string",
+ * required=false
+ * ),
* @SWG\Response(
* response=200,
* description="successful operation",
@@ -496,6 +513,7 @@ public function edit(Request $request, Response $response, $id)
$command->description = $sanitizedParams->getString('description');
$command->commandString = $sanitizedParams->getString('commandString');
$command->validationString = $sanitizedParams->getString('validationString');
+ $command->createAlertOn = $sanitizedParams->getString('createAlertOn', ['default' => 'never']);
$availableOn = $sanitizedParams->getArray('availableOn');
if (empty($availableOn)) {
$command->availableOn = null;
@@ -552,7 +570,7 @@ public function delete(Request $request, Response $response, $id)
throw new AccessDeniedException();
}
- $this->getDispatcher()->dispatch(CommandDeleteEvent::$NAME, new CommandDeleteEvent($command));
+ $this->getDispatcher()->dispatch(new CommandDeleteEvent($command), CommandDeleteEvent::$NAME);
$command->delete();
diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php
index 2a942c037a..4928791744 100644
--- a/lib/Controller/Display.php
+++ b/lib/Controller/Display.php
@@ -2474,6 +2474,8 @@ public function validateDisplays($displays)
$event = $this->displayEventFactory->createEmpty();
$event->displayId = $display->displayId;
$event->start = $display->lastAccessed;
+ // eventTypeId 1 is for Display up/down events.
+ $event->eventTypeId = 1;
$event->save();
}
diff --git a/lib/Controller/DisplayProfile.php b/lib/Controller/DisplayProfile.php
index 3c548c01b4..3d27d9a93a 100644
--- a/lib/Controller/DisplayProfile.php
+++ b/lib/Controller/DisplayProfile.php
@@ -466,6 +466,7 @@ public function edit(Request $request, Response $response, $id)
// Set and assign the command
$command->commandString = $parsedParams->getString('commandString_' . $command->commandId);
$command->validationString = $parsedParams->getString('validationString_' . $command->commandId);
+ $command->createAlertOn = $parsedParams->getString('createAlertOn_' . $command->commandId);
$displayProfile->assignCommand($command);
} else {
diff --git a/lib/Entity/Command.php b/lib/Entity/Command.php
index 5eefbb2e9c..90fd0d04ec 100644
--- a/lib/Entity/Command.php
+++ b/lib/Entity/Command.php
@@ -1,8 +1,8 @@
validationStringDisplayProfile) ? $this->validationString : $this->validationStringDisplayProfile;
+ return empty($this->validationStringDisplayProfile)
+ ? $this->validationString
+ : $this->validationStringDisplayProfile;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCreateAlertOn(): string
+ {
+ return empty($this->createAlertOnDisplayProfile)
+ ? $this->createAlertOn
+ : $this->createAlertOnDisplayProfile;
}
/**
@@ -210,18 +237,24 @@ public function isReady()
public function validate()
{
if (!v::stringType()->notEmpty()->length(1, 254)->validate($this->command)) {
- throw new InvalidArgumentException(__('Please enter a command name between 1 and 254 characters'),
- 'command');
+ throw new InvalidArgumentException(
+ __('Please enter a command name between 1 and 254 characters'),
+ 'command'
+ );
}
if (!v::alpha('_')->NoWhitespace()->notEmpty()->length(1, 50)->validate($this->code)) {
- throw new InvalidArgumentException(__('Please enter a code between 1 and 50 characters containing only alpha characters and no spaces'),
- 'code');
+ throw new InvalidArgumentException(
+ __('Please enter a code between 1 and 50 characters containing only alpha characters and no spaces'),
+ 'code'
+ );
}
if (!v::stringType()->length(0, 1000)->validate($this->description)) {
- throw new InvalidArgumentException(__('Please enter a description between 1 and 1000 characters'),
- 'description');
+ throw new InvalidArgumentException(
+ __('Please enter a description between 1 and 1000 characters'),
+ 'description'
+ );
}
}
@@ -251,14 +284,35 @@ public function save($options = [])
*/
public function delete()
{
- $this->getStore()->update('DELETE FROM `command` WHERE `commandId` = :commandId', ['commandId' => $this->commandId]);
+ $this->getStore()->update(
+ 'DELETE FROM `command` WHERE `commandId` = :commandId',
+ ['commandId' => $this->commandId]
+ );
}
private function add()
{
$this->commandId = $this->getStore()->insert('
- INSERT INTO `command` (`command`, `code`, `description`, `userId`, `commandString`, `validationString`, `availableOn`)
- VALUES (:command, :code, :description, :userId, :commandString, :validationString, :availableOn)
+ INSERT INTO `command` (
+ `command`,
+ `code`,
+ `description`,
+ `userId`,
+ `commandString`,
+ `validationString`,
+ `availableOn`,
+ `createAlertOn`
+ )
+ VALUES (
+ :command,
+ :code,
+ :description,
+ :userId,
+ :commandString,
+ :validationString,
+ :availableOn,
+ :createAlertOn
+ )
', [
'command' => $this->command,
'code' => $this->code,
@@ -266,7 +320,8 @@ private function add()
'userId' => $this->userId,
'commandString' => $this->commandString,
'validationString' => $this->validationString,
- 'availableOn' => $this->availableOn
+ 'availableOn' => $this->availableOn,
+ 'createAlertOn' => $this->createAlertOn
]);
}
@@ -280,7 +335,8 @@ private function edit()
`userId` = :userId,
`commandString` = :commandString,
`validationString` = :validationString,
- `availableOn` = :availableOn
+ `availableOn` = :availableOn,
+ `createAlertOn` = :createAlertOn
WHERE `commandId` = :commandId
', [
'command' => $this->command,
@@ -290,7 +346,8 @@ private function edit()
'commandId' => $this->commandId,
'commandString' => $this->commandString,
'validationString' => $this->validationString,
- 'availableOn' => $this->availableOn
+ 'availableOn' => $this->availableOn,
+ 'createAlertOn' => $this->createAlertOn
]);
}
}
\ No newline at end of file
diff --git a/lib/Entity/DisplayEvent.php b/lib/Entity/DisplayEvent.php
index 4d6f814461..094bfc60dd 100644
--- a/lib/Entity/DisplayEvent.php
+++ b/lib/Entity/DisplayEvent.php
@@ -1,8 +1,8 @@
.
*/
-
namespace Xibo\Entity;
+
use Carbon\Carbon;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
@@ -39,55 +40,181 @@ class DisplayEvent implements \JsonSerializable
public $eventDate;
public $start;
public $end;
+ public $eventTypeId;
+ public $refId;
+ public $detail;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
+ * @param EventDispatcherInterface $dispatcher
*/
- public function __construct($store, $log, $dispatcher)
- {
+ public function __construct(
+ StorageServiceInterface $store,
+ LogServiceInterface $log,
+ EventDispatcherInterface $dispatcher
+ ) {
$this->setCommonDependencies($store, $log, $dispatcher);
}
- public function save()
+ /**
+ * Save displayevent
+ * @return void
+ */
+ public function save(): void
{
- if ($this->displayEventId == null)
+ if ($this->displayEventId == null) {
$this->add();
- else
+ } else {
$this->edit();
+ }
}
- private function add()
+ /**
+ * Add a new displayevent
+ * @return void
+ */
+ private function add(): void
{
$this->displayEventId = $this->getStore()->insert('
- INSERT INTO `displayevent` (eventDate, start, end, displayID)
- VALUES (:eventDate, :start, :end, :displayId)
+ INSERT INTO `displayevent` (eventDate, start, end, displayID, eventTypeId, refId, detail)
+ VALUES (:eventDate, :start, :end, :displayId, :eventTypeId, :refId, :detail)
', [
'eventDate' => Carbon::now()->format('U'),
'start' => $this->start,
'end' => $this->end,
- 'displayId' => $this->displayId
+ 'displayId' => $this->displayId,
+ 'eventTypeId' => $this->eventTypeId,
+ 'refId' => $this->refId,
+ 'detail' => $this->detail,
]);
}
- private function edit()
+ /**
+ * Edit displayevent
+ * @return void
+ */
+ private function edit(): void
{
- $this->getStore()->update('UPDATE `displayevent` SET `end` = :end WHERE statId = :statId', [
- 'displayevent' => $this->displayEventId, 'end' => $this->end
+ $this->getStore()->update('
+ UPDATE displayevent
+ SET end = :end,
+ displayId = :displayId,
+ eventTypeId = :eventTypeId,
+ refId = :refId,
+ detail = :detail
+ WHERE displayEventId = :displayEventId
+ ', [
+ 'displayEventId' => $this->displayEventId,
+ 'end' => $this->end,
+ 'displayId' => $this->displayId,
+ 'eventTypeId' => $this->eventTypeId,
+ 'refId' => $this->refId,
+ 'detail' => $this->detail,
]);
}
+
/**
- * Record the display coming online
- * @param $displayId
+ * Record end date for specified display and event type.
+ * @param int $displayId
+ * @param int|null $date
+ * @param int $eventTypeId
+ * @return void
*/
- public function displayUp($displayId)
+ public function eventEnd(int $displayId, int $eventTypeId = 1, ?int $date = null): void
{
- $this->getStore()->update('UPDATE `displayevent` SET `end` = :toDt WHERE displayId = :displayId AND `end` IS NULL', [
- 'toDt' => Carbon::now()->format('U'),
- 'displayId' => $displayId
- ]);
+ $this->getLog()->debug(
+ sprintf(
+ 'displayEvent : end display alert for eventType %s and displayId %d',
+ $this->getEventNameFromId($eventTypeId),
+ $displayId
+ )
+ );
+
+ $this->getStore()->update(
+ 'UPDATE `displayevent` SET `end` = :toDt
+ WHERE displayId = :displayId
+ AND `end` IS NULL
+ AND eventTypeId = :eventTypeId',
+ [
+ 'toDt' => $date ?? Carbon::now()->format('U'),
+ 'displayId' => $displayId,
+ 'eventTypeId' => $eventTypeId,
+ ]
+ );
+ }
+
+ /**
+ * Record end date for specified display, event type and refId
+ * @param int $displayId
+ * @param int $eventTypeId
+ * @param int $refId
+ * @param int|null $date
+ * @return void
+ */
+ public function eventEndByReference(int $displayId, int $eventTypeId, int $refId, ?int $date = null): void
+ {
+ $this->getLog()->debug(
+ sprintf(
+ 'displayEvent : end display alert for refId %d, displayId %d and eventType %s',
+ $refId,
+ $displayId,
+ $this->getEventNameFromId($eventTypeId),
+ )
+ );
+
+ $this->getStore()->update(
+ 'UPDATE `displayevent` SET `end` = :toDt
+ WHERE displayId = :displayId
+ AND `end` IS NULL
+ AND eventTypeId = :eventTypeId
+ AND refId = :refId',
+ [
+ 'toDt' => $date ?? Carbon::now()->format('U'),
+ 'displayId' => $displayId,
+ 'eventTypeId' => $eventTypeId,
+ 'refId' => $refId,
+ ]
+ );
+ }
+
+ /**
+ * Match event type string from log to eventTypeId in database.
+ * @param string $eventType
+ * @return int
+ */
+ public function getEventIdFromString(string $eventType): int
+ {
+ return match ($eventType) {
+ 'Display Up/down' => 1,
+ 'App Start' => 2,
+ 'Power Cycle' => 3,
+ 'Network Cycle' => 4,
+ 'TV Monitoring' => 5,
+ 'Player Fault' => 6,
+ 'Command' => 7,
+ default => 8
+ };
+ }
+
+ /**
+ * Match eventTypeId from database to string event name.
+ * @param int $eventTypeId
+ * @return string
+ */
+ public function getEventNameFromId(int $eventTypeId): string
+ {
+ return match ($eventTypeId) {
+ 1 => __('Display Up/down'),
+ 2 => __('App Start'),
+ 3 => __('Power Cycle'),
+ 4 => __('Network Cycle'),
+ 5 => __('TV Monitoring'),
+ 6 => __('Player Fault'),
+ 7 => __('Command'),
+ default => __('Other')
+ };
}
-}
\ No newline at end of file
+}
diff --git a/lib/Entity/DisplayProfile.php b/lib/Entity/DisplayProfile.php
index 9755331002..034baa7d54 100644
--- a/lib/Entity/DisplayProfile.php
+++ b/lib/Entity/DisplayProfile.php
@@ -1,6 +1,6 @@
getId() == $command->getId()) {
$alreadyAssigned->commandString = $command->commandString;
$alreadyAssigned->validationString = $command->validationString;
+ $alreadyAssigned->createAlertOn = $command->createAlertOn;
$assigned = true;
break;
}
}
- if (!$assigned)
+ if (!$assigned) {
$this->commands[] = $command;
+ }
}
/**
@@ -481,22 +483,41 @@ private function manageAssignments()
foreach ($this->commands as $command) {
/* @var Command $command */
$this->getStore()->update('
- INSERT INTO `lkcommanddisplayprofile` (`commandId`, `displayProfileId`, `commandString`, `validationString`) VALUES
- (:commandId, :displayProfileId, :commandString, :validationString) ON DUPLICATE KEY UPDATE commandString = :commandString2, validationString = :validationString2
+ INSERT INTO `lkcommanddisplayprofile` (
+ `commandId`,
+ `displayProfileId`,
+ `commandString`,
+ `validationString`,
+ `createAlertOn`
+ )
+ VALUES (
+ :commandId,
+ :displayProfileId,
+ :commandString,
+ :validationString,
+ :createAlertOn
+ )
+ ON DUPLICATE KEY UPDATE
+ commandString = :commandString2,
+ validationString = :validationString2,
+ createAlertOn = :createAlertOn2
', [
'commandId' => $command->commandId,
'displayProfileId' => $this->displayProfileId,
'commandString' => $command->commandString,
'validationString' => $command->validationString,
+ 'createAlertOn' => $command->createAlertOn,
'commandString2' => $command->commandString,
- 'validationString2' => $command->validationString
+ 'validationString2' => $command->validationString,
+ 'createAlertOn2' => $command->createAlertOn
]);
}
// Unlink
$params = ['displayProfileId' => $this->displayProfileId];
- $sql = 'DELETE FROM `lkcommanddisplayprofile` WHERE `displayProfileId` = :displayProfileId AND `commandId` NOT IN (0';
+ $sql = 'DELETE FROM `lkcommanddisplayprofile`
+ WHERE `displayProfileId` = :displayProfileId AND `commandId` NOT IN (0';
$i = 0;
foreach ($this->commands as $command) {
@@ -554,7 +575,10 @@ public function getCustomEditTemplate()
if ($this->isCustom()) {
return $this->displayProfileFactory->getCustomEditTemplate($this->getClientType());
} else {
- $this->getLog()->error('Attempting to get Custom Edit template for Display Profile ' . $this->getClientType() . ' that is not custom');
+ $this->getLog()->error(
+ 'Attempting to get Custom Edit template for Display Profile ' .
+ $this->getClientType() . ' that is not custom'
+ );
return null;
}
}
diff --git a/lib/Factory/CommandFactory.php b/lib/Factory/CommandFactory.php
index 19c6e3b558..5991e94c61 100644
--- a/lib/Factory/CommandFactory.php
+++ b/lib/Factory/CommandFactory.php
@@ -117,16 +117,19 @@ public function query($sortOrder = null, $filterBy = [])
`command`.userId,
`command`.availableOn,
`command`.commandString,
- `command`.validationString ';
+ `command`.validationString,
+ `command`.createAlertOn
+ ';
if ($sanitizedFilter->getInt('displayProfileId') !== null) {
$select .= ',
:displayProfileId AS displayProfileId,
`lkcommanddisplayprofile`.commandString AS commandStringDisplayProfile,
- `lkcommanddisplayprofile`.validationString AS validationStringDisplayProfile ';
+ `lkcommanddisplayprofile`.validationString AS validationStringDisplayProfile,
+ `lkcommanddisplayprofile`.createAlertOn AS createAlertOnDisplayProfile ';
}
- $select .= " , (SELECT GROUP_CONCAT(DISTINCT `group`.group)
+ $select .= ' , (SELECT GROUP_CONCAT(DISTINCT `group`.group)
FROM `permission`
INNER JOIN `permissionentity`
ON `permissionentity`.entityId = permission.entityId
@@ -135,7 +138,7 @@ public function query($sortOrder = null, $filterBy = [])
WHERE entity = :permissionEntityForGroup
AND objectId = command.commandId
AND view = 1
- ) AS groupsWithPermissions ";
+ ) AS groupsWithPermissions ';
$params['permissionEntityForGroup'] = 'Xibo\\Entity\\Command';
$body = ' FROM `command` ';
@@ -195,7 +198,14 @@ public function query($sortOrder = null, $filterBy = [])
$params['userId'] = $sanitizedFilter->getInt('userId');
}
- $this->viewPermissionSql('Xibo\Entity\Command', $body, $params, 'command.commandId', 'command.userId', $filterBy);
+ $this->viewPermissionSql(
+ 'Xibo\Entity\Command',
+ $body,
+ $params,
+ 'command.commandId',
+ 'command.userId',
+ $filterBy
+ );
// Sorting?
$order = '';
diff --git a/lib/Report/DisplayAlerts.php b/lib/Report/DisplayAlerts.php
new file mode 100644
index 0000000000..37abf820e3
--- /dev/null
+++ b/lib/Report/DisplayAlerts.php
@@ -0,0 +1,353 @@
+.
+ */
+
+namespace Xibo\Report;
+
+use Carbon\Carbon;
+use Psr\Container\ContainerInterface;
+use Xibo\Controller\DataTablesDotNetTrait;
+use Xibo\Entity\ReportForm;
+use Xibo\Entity\ReportResult;
+use Xibo\Entity\ReportSchedule;
+use Xibo\Factory\DisplayEventFactory;
+use Xibo\Factory\DisplayFactory;
+use Xibo\Factory\DisplayGroupFactory;
+use Xibo\Helper\DateFormatHelper;
+use Xibo\Helper\Translate;
+use Xibo\Support\Sanitizer\SanitizerInterface;
+
+/**
+ * Class DisplayAlerts
+ * @package Xibo\Report
+ */
+class DisplayAlerts implements ReportInterface
+{
+ use ReportDefaultTrait, DataTablesDotNetTrait;
+
+ /** @var DisplayFactory */
+ private $displayFactory;
+ /** @var DisplayGroupFactory */
+ private $displayGroupFactory;
+ /** @var DisplayEventFactory */
+ private $displayEventFactory;
+
+ public function setFactories(ContainerInterface $container)
+ {
+ $this->displayFactory = $container->get('displayFactory');
+ $this->displayGroupFactory = $container->get('displayGroupFactory');
+ $this->displayEventFactory = $container->get('displayEventFactory');
+
+ return $this;
+ }
+
+ public function getReportEmailTemplate()
+ {
+ return 'displayalerts-email-template.twig';
+ }
+
+ public function getSavedReportTemplate()
+ {
+ return 'displayalerts-report-preview';
+ }
+
+ public function getReportForm()
+ {
+ return new ReportForm(
+ 'displayalerts-report-form',
+ 'displayalerts',
+ 'Display',
+ [
+ 'fromDate' => Carbon::now()->startOfMonth()->format(DateFormatHelper::getSystemFormat()),
+ 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
+ ]
+ );
+ }
+
+ public function getReportScheduleFormData(SanitizerInterface $sanitizedParams)
+ {
+ $data = [];
+ $data['reportName'] = 'displayalerts';
+
+ return [
+ 'template' => 'displayalerts-schedule-form-add',
+ 'data' => $data
+ ];
+ }
+
+ public function setReportScheduleFormData(SanitizerInterface $sanitizedParams)
+ {
+ $filter = $sanitizedParams->getString('filter');
+ $displayId = $sanitizedParams->getInt('displayId');
+ $displayGroupIds = $sanitizedParams->getIntArray('displayGroupId', ['default' => []]);
+ $filterCriteria['displayId'] = $displayId;
+
+ if (empty($displayId) && count($displayGroupIds) > 0) {
+ $filterCriteria['displayGroupId'] = $displayGroupIds;
+ }
+
+ $filterCriteria['filter'] = $filter;
+
+ $schedule = '';
+ if ($filter == 'daily') {
+ $schedule = ReportSchedule::$SCHEDULE_DAILY;
+ $filterCriteria['reportFilter'] = 'yesterday';
+ } elseif ($filter == 'weekly') {
+ $schedule = ReportSchedule::$SCHEDULE_WEEKLY;
+ $filterCriteria['reportFilter'] = 'lastweek';
+ } elseif ($filter == 'monthly') {
+ $schedule = ReportSchedule::$SCHEDULE_MONTHLY;
+ $filterCriteria['reportFilter'] = 'lastmonth';
+ } elseif ($filter == 'yearly') {
+ $schedule = ReportSchedule::$SCHEDULE_YEARLY;
+ $filterCriteria['reportFilter'] = 'lastyear';
+ }
+
+ $filterCriteria['sendEmail'] = $sanitizedParams->getCheckbox('sendEmail');
+ $filterCriteria['nonusers'] = $sanitizedParams->getString('nonusers');
+
+ // Return
+ return [
+ 'filterCriteria' => json_encode($filterCriteria),
+ 'schedule' => $schedule
+ ];
+ }
+
+ public function generateSavedReportName(SanitizerInterface $sanitizedParams)
+ {
+ return sprintf(__('%s report for Display'), ucfirst($sanitizedParams->getString('filter')));
+ }
+
+ public function restructureSavedReportOldJson($json)
+ {
+ return $json;
+ }
+
+ public function getSavedReportResults($json, $savedReport)
+ {
+ $metadata = [
+ 'periodStart' => $json['metadata']['periodStart'],
+ 'periodEnd' => $json['metadata']['periodEnd'],
+ 'generatedOn' => Carbon::createFromTimestamp($savedReport->generatedOn)
+ ->format(DateFormatHelper::getSystemFormat()),
+ 'title' => $savedReport->saveAs,
+ ];
+
+ // Report result object
+ return new ReportResult(
+ $metadata,
+ $json['table'],
+ $json['recordsTotal'],
+ );
+ }
+
+ public function getResults(SanitizerInterface $sanitizedParams)
+ {
+ $displayIds = $this->getDisplayIdFilter($sanitizedParams);
+ $onlyLoggedIn = $sanitizedParams->getCheckbox('onlyLoggedIn') == 1;
+
+ $currentDate = Carbon::now()->startOfDay();
+
+ //
+ // From and To Date Selection
+ // --------------------------
+ // Our report has a range filter which determines whether the user has to enter their own from / to dates
+ // check the range filter first and set from/to dates accordingly.
+ $reportFilter = $sanitizedParams->getString('reportFilter');
+
+ // Use the current date as a helper
+ $now = Carbon::now();
+
+ switch ($reportFilter) {
+ // the monthly data starts from yesterday
+ case 'yesterday':
+ $fromDt = $now->copy()->startOfDay()->subDay();
+ $toDt = $now->copy()->startOfDay();
+ break;
+
+ case 'lastweek':
+ $fromDt = $now->copy()->locale(Translate::GetLocale())->startOfWeek()->subWeek();
+ $toDt = $fromDt->copy()->addWeek();
+ break;
+
+ case 'lastmonth':
+ $fromDt = $now->copy()->startOfMonth()->subMonth();
+ $toDt = $fromDt->copy()->addMonth();
+ break;
+
+ case 'lastyear':
+ $fromDt = $now->copy()->startOfYear()->subYear();
+ $toDt = $fromDt->copy()->addYear();
+ break;
+
+ case '':
+ default:
+ // Expect dates to be provided.
+ $fromDt = $sanitizedParams->getDate('fromDt');
+ $toDt = $sanitizedParams->getDate('toDt');
+
+ $fromDt = $fromDt->startOfDay();
+
+ // If toDt is current date then make it current datetime
+ if ($toDt->format('Y-m-d') == $currentDate->format('Y-m-d')) {
+ $toDt = Carbon::now();
+ } else {
+ $toDt = $toDt->addDay()->startOfDay();
+ }
+
+ break;
+ }
+
+ $metadata = [
+ 'periodStart' => Carbon::createFromTimestamp($fromDt->toDateTime()->format('U'))
+ ->format(DateFormatHelper::getSystemFormat()),
+ 'periodEnd' => Carbon::createFromTimestamp($toDt->toDateTime()->format('U'))
+ ->format(DateFormatHelper::getSystemFormat()),
+ ];
+
+ $params = [
+ 'start' => $fromDt->format('U'),
+ 'end' => $toDt->format('U')
+ ];
+
+ $sql = 'SELECT
+ `displayevent`.displayId,
+ `display`.display,
+ `displayevent`.start,
+ `displayevent`.end,
+ `displayevent`.eventTypeId,
+ `displayevent`.refId,
+ `displayevent`.detail
+ FROM `displayevent`
+ INNER JOIN `display` ON `display`.displayId = `displayevent`.displayId
+ INNER JOIN `lkdisplaydg` ON `display`.displayId = `lkdisplaydg`.displayId
+ INNER JOIN `displaygroup` ON `displaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId
+ AND `displaygroup`.isDisplaySpecific = 1
+ WHERE `displayevent`.eventDate BETWEEN :start AND :end ';
+
+ if (count($displayIds) > 0) {
+ $sql .= 'AND `displayevent`.displayId IN (' . implode(',', $displayIds) . ')';
+ }
+
+ if ($onlyLoggedIn) {
+ $sql .= ' AND `display`.loggedIn = 1 ';
+ }
+
+ // Tags
+ if (!empty($sanitizedParams->getString('tags'))) {
+ $tagFilter = $sanitizedParams->getString('tags');
+
+ if (trim($tagFilter) === '--no-tag') {
+ $sql .= ' AND `displaygroup`.displaygroupId NOT IN (
+ SELECT `lktagdisplaygroup`.displaygroupId
+ FROM tag
+ INNER JOIN `lktagdisplaygroup`
+ ON `lktagdisplaygroup`.tagId = tag.tagId
+ )
+ ';
+ } else {
+ $operator = $sanitizedParams->getCheckbox('exactTags') == 1 ? '=' : 'LIKE';
+ $logicalOperator = $sanitizedParams->getString('logicalOperator', ['default' => 'OR']);
+ $allTags = explode(',', $tagFilter);
+ $notTags = [];
+ $tags = [];
+
+ foreach ($allTags as $tag) {
+ if (str_starts_with($tag, '-')) {
+ $notTags[] = ltrim(($tag), '-');
+ } else {
+ $tags[] = $tag;
+ }
+ }
+
+ if (!empty($notTags)) {
+ $sql .= ' AND `displaygroup`.displaygroupId NOT IN (
+ SELECT `lktagdisplaygroup`.displaygroupId
+ FROM tag
+ INNER JOIN `lktagdisplaygroup`
+ ON `lktagdisplaygroup`.tagId = tag.tagId
+ ';
+
+ $this->displayFactory->tagFilter(
+ $notTags,
+ 'lktagdisplaygroup',
+ 'lkTagDisplayGroupId',
+ 'displayGroupId',
+ $logicalOperator,
+ $operator,
+ true,
+ $sql,
+ $params
+ );
+ }
+
+ if (!empty($tags)) {
+ $sql .= ' AND `displaygroup`.displaygroupId IN (
+ SELECT `lktagdisplaygroup`.displaygroupId
+ FROM tag
+ INNER JOIN `lktagdisplaygroup`
+ ON `lktagdisplaygroup`.tagId = tag.tagId
+ ';
+
+ $this->displayFactory->tagFilter(
+ $tags,
+ 'lktagdisplaygroup',
+ 'lkTagDisplayGroupId',
+ 'displayGroupId',
+ $logicalOperator,
+ $operator,
+ false,
+ $sql,
+ $params
+ );
+ }
+ }
+ }
+
+ // Sorting?
+ $sortOrder = $this->gridRenderSort($sanitizedParams);
+
+ if (is_array($sortOrder)) {
+ $sql .= 'ORDER BY ' . implode(',', $sortOrder);
+ }
+
+ $rows = [];
+ foreach ($this->store->select($sql, $params) as $row) {
+ $displayEvent = $this->displayEventFactory->createEmpty()->hydrate($row);
+ $displayEvent->setUnmatchedProperty(
+ 'eventType',
+ $displayEvent->getEventNameFromId($displayEvent->eventTypeId)
+ );
+ $displayEvent->setUnmatchedProperty(
+ 'display',
+ $row['display']
+ );
+
+ $rows[] = $displayEvent;
+ }
+
+ return new ReportResult(
+ $metadata,
+ $rows,
+ count($rows),
+ );
+ }
+}
diff --git a/lib/Report/TimeConnected.php b/lib/Report/TimeConnected.php
index 64080a8d9f..da3eb0e07a 100644
--- a/lib/Report/TimeConnected.php
+++ b/lib/Report/TimeConnected.php
@@ -1,8 +1,8 @@
0) {
- $query .= ' WHERE display.displayID IN (' . implode(',', $displayIds) . ') ';
+ $query .= ' AND display.displayID IN (' . implode(',', $displayIds) . ') ';
}
$query .= '
diff --git a/lib/Report/TimeDisconnectedSummary.php b/lib/Report/TimeDisconnectedSummary.php
index ce261f2962..ef3fe494a5 100644
--- a/lib/Report/TimeDisconnectedSummary.php
+++ b/lib/Report/TimeDisconnectedSummary.php
@@ -1,6 +1,6 @@
= :start
- AND :end <= UNIX_TIMESTAMP(NOW()) ';
+ AND :end <= UNIX_TIMESTAMP(NOW())
+ AND `displayevent`.eventTypeId = 1 ';
if (count($displayIds) > 0) {
$body .= 'AND display.displayId IN (' . implode(',', $displayIds) . ') ';
diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php
index 6aafba9fa0..b255b9e2e0 100644
--- a/lib/Xmds/Soap.php
+++ b/lib/Xmds/Soap.php
@@ -43,6 +43,7 @@
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\NotificationFactory;
+use Xibo\Factory\PlayerFaultFactory;
use Xibo\Factory\PlayerVersionFactory;
use Xibo\Factory\RegionFactory;
use Xibo\Factory\RequiredFileFactory;
@@ -167,6 +168,9 @@ class Soap
/** @var \Xibo\Factory\CampaignFactory */
private $campaignFactory;
+ /** @var PlayerFaultFactory */
+ protected $playerFaultFactory;
+
/**
* Soap constructor.
* @param LogProcessor $logProcessor
@@ -220,7 +224,8 @@ public function __construct(
$playerVersionFactory,
$dispatcher,
$campaignFactory,
- $syncGroupFactory
+ $syncGroupFactory,
+ $playerFaultFactory
) {
$this->logProcessor = $logProcessor;
$this->pool = $pool;
@@ -247,6 +252,7 @@ public function __construct(
$this->dispatcher = $dispatcher;
$this->campaignFactory = $campaignFactory;
$this->syncGroupFactory = $syncGroupFactory;
+ $this->playerFaultFactory = $playerFaultFactory;
}
/**
@@ -1737,6 +1743,14 @@ protected function doSubmitLog($serverKey, $hardwareKey, $logXml)
continue;
}
+ // special handling for event
+ // this will create record in displayevent table
+ // and is not added to the logs.
+ if ($cat == 'event') {
+ $this->createDisplayAlert($node);
+ continue;
+ }
+
// Does this meet the current log level?
if ($cat == 'error') {
$recordLogLevel = Logger::ERROR;
@@ -1758,25 +1772,7 @@ protected function doSubmitLog($serverKey, $hardwareKey, $logXml)
}
// Adjust the date according to the display timezone
- try {
- $date = ($this->display->timeZone != null)
- ? Carbon::createFromFormat(
- DateFormatHelper::getSystemFormat(),
- $date,
- $this->display->timeZone
- )->tz($defaultTimeZone)
- : Carbon::createFromFormat(
- DateFormatHelper::getSystemFormat(),
- $date
- );
- $date = $date->format(DateFormatHelper::getSystemFormat());
- } catch (\Exception $e) {
- // Protect against the date format being unreadable
- $this->getLog()->debug('Date format unreadable on log message: ' . $date);
-
- // Use now instead
- $date = Carbon::now()->format(DateFormatHelper::getSystemFormat());
- }
+ $date = $this->adjustDisplayLogDate($date, DateFormatHelper::getSystemFormat());
// Get the date and the message (all log types have these)
foreach ($node->childNodes as $nodeElements) {
@@ -2657,12 +2653,11 @@ protected function alertDisplayUp()
{
$maintenanceEnabled = $this->getConfig()->getSetting('MAINTENANCE_ENABLED');
- if ($this->display->loggedIn == 0) {
-
+ if ($this->display->loggedIn == 0 && !empty($this->display->displayId)) {
$this->getLog()->info(sprintf('Display %s was down, now its up.', $this->display->display));
// Log display up
- $this->displayEventFactory->createEmpty()->displayUp($this->display->displayId);
+ $this->displayEventFactory->createEmpty()->eventEnd($this->display->displayId);
$dayPartId = $this->display->getSetting('dayPartId', null,['displayOverride' => true]);
@@ -3041,4 +3036,86 @@ protected function setDateFilters()
$this->getLog()->debug(sprintf('FromDT = %s [%d]. ToDt = %s [%d]', $fromFilter->toRssString(), $fromFilter->format('U'), $toFilter->toRssString(), $toFilter->format('U')));
}
+
+ /**
+ * Adjust the log date according to the Display timezone.
+ * Return current date if we fail.
+ * @param string $date
+ * @param string $format
+ * @return string
+ */
+ protected function adjustDisplayLogDate(string $date, string $format): string
+ {
+ // Get the display timezone to use when adjusting log dates.
+ $defaultTimeZone = $this->getConfig()->getSetting('defaultTimezone');
+
+ // Adjust the date according to the display timezone
+ try {
+ $date = ($this->display->timeZone != null)
+ ? Carbon::createFromFormat(
+ DateFormatHelper::getSystemFormat(),
+ $date,
+ $this->display->timeZone
+ )->tz($defaultTimeZone)
+ : Carbon::createFromFormat(
+ DateFormatHelper::getSystemFormat(),
+ $date
+ );
+ $date = $date->format($format);
+ } catch (\Exception $e) {
+ // Protect against the date format being unreadable
+ $this->getLog()->debug('Date format unreadable on log message: ' . $date);
+
+ // Use now instead
+ $date = Carbon::now()->format($format);
+ }
+
+ return $date;
+ }
+
+ private function createDisplayAlert(\DomElement $alertNode)
+ {
+ $date = $this->adjustDisplayLogDate($alertNode->getAttribute('date'), 'U');
+ $eventType = '';
+ $refId = '';
+ $detail = '';
+ $alertType = '';
+
+ // Get the nodes we are expecting
+ foreach ($alertNode->childNodes as $nodeElements) {
+ if ($nodeElements->nodeName == 'eventType') {
+ $eventType = $nodeElements->textContent;
+ } else if ($nodeElements->nodeName == 'refId') {
+ $refId = $nodeElements->textContent;
+ } else if ($nodeElements->nodeName == 'message') {
+ $detail = $nodeElements->textContent;
+ } else if ($nodeElements->nodeName == 'alertType') {
+ $alertType = $nodeElements->textContent;
+ }
+ }
+
+ // if alerts should provide both start and end or just start
+ if ($alertType == 'both' || $alertType == 'start') {
+ $displayEvent = $this->displayEventFactory->createEmpty();
+
+ // new record populated from the submitLog xml.
+ $displayEvent->displayId = $this->display->displayId;
+ $displayEvent->eventTypeId = $displayEvent->getEventIdFromString($eventType);
+ $displayEvent->eventDate = $date;
+ $displayEvent->start = $date;
+ $displayEvent->end = $alertType == 'both' ? $date : null;
+ $displayEvent->refId = empty($refId) ? null : $refId;
+ $displayEvent->detail = $detail;
+
+ $displayEvent->save();
+ } else if ($alertType == 'end') {
+ // if this event pertain only to end date for an existing event record,
+ // then set the end date for this display and the specified eventType
+ $displayEvent = $this->displayEventFactory->createEmpty();
+ $eventTypeId = $displayEvent->getEventIdFromString($eventType);
+ empty($refId)
+ ? $displayEvent->eventEnd($this->display->displayId, $eventTypeId, $date)
+ : $displayEvent->eventEndByReference($this->display->displayId, $eventTypeId, $refId);
+ }
+ }
}
diff --git a/lib/Xmds/Soap5.php b/lib/Xmds/Soap5.php
index 7d898c3ffa..fdbba867a6 100644
--- a/lib/Xmds/Soap5.php
+++ b/lib/Xmds/Soap5.php
@@ -329,6 +329,7 @@ public function RegisterDisplay(
}
$node = $return->createElement($command->code);
+ $node->setAttribute('createAlertOn', $command->getCreateAlertOn());
$commandString = $return->createElement('commandString');
$commandStringCData = $return->createCDATASection($command->getCommandString());
$commandString->appendChild($commandStringCData);
diff --git a/lib/Xmds/Soap6.php b/lib/Xmds/Soap6.php
index 0dfc7acdbb..e053332a97 100644
--- a/lib/Xmds/Soap6.php
+++ b/lib/Xmds/Soap6.php
@@ -25,6 +25,7 @@
use Carbon\Carbon;
use Xibo\Entity\Bandwidth;
use Xibo\Helper\DateFormatHelper;
+use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Class Soap6
@@ -57,7 +58,10 @@ public function reportFaults(string $serverKey, string $hardwareKey, string $fau
// Check the serverKey matches
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
- throw new \SoapFault('Sender', 'The Server key you entered does not match with the server key at this address');
+ throw new \SoapFault(
+ 'Sender',
+ 'The Server key you entered does not match with the server key at this address'
+ );
}
// Auth this request...
@@ -70,17 +74,23 @@ public function reportFaults(string $serverKey, string $hardwareKey, string $fau
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
}
+ $faultDecoded = json_decode($fault, true);
+
+ // check if we should record or update any display events.
+ $this->manageDisplayAlerts($faultDecoded);
+
// clear existing records from player_faults table
$this->getStore()->update('DELETE FROM `player_faults` WHERE displayId = :displayId', [
'displayId' => $this->display->displayId
]);
- $faultDecoded = json_decode($fault, true);
-
foreach ($faultDecoded as $faultAlert) {
$sanitizedFaultAlert = $this->getSanitizer($faultAlert);
- $incidentDt = $sanitizedFaultAlert->getDate('date', ['default' => Carbon::now()])->format(DateFormatHelper::getSystemFormat());
+ $incidentDt = $sanitizedFaultAlert->getDate(
+ 'date',
+ ['default' => Carbon::now()]
+ )->format(DateFormatHelper::getSystemFormat());
$expires = $sanitizedFaultAlert->getDate('expires', ['default' => null]);
$code = $sanitizedFaultAlert->getInt('code');
$reason = $sanitizedFaultAlert->getString('reason');
@@ -125,4 +135,99 @@ public function reportFaults(string $serverKey, string $hardwareKey, string $fau
return true;
}
+
+ private function manageDisplayAlerts(array $newPlayerFaults)
+ {
+ // check current faults for player
+ $currentFaults = $this->playerFaultFactory->getByDisplayId($this->display->displayId);
+
+ // if we had faults and now we no longer have any to add
+ // set end date for any existing fault events
+ // add display event for cleared all faults
+ if (!empty($currentFaults) && empty($newPlayerFaults)) {
+ $displayEvent = $this->displayEventFactory->createEmpty();
+ $displayEvent->eventTypeId = $displayEvent->getEventIdFromString('Player Fault');
+ $displayEvent->displayId = $this->display->displayId;
+ // clear any open player fault events for this display
+ $displayEvent->eventEnd($displayEvent->displayId, $displayEvent->eventTypeId);
+
+ // log new event for all faults cleared.
+ $displayEvent->start = Carbon::now()->format('U');
+ $displayEvent->end = Carbon::now()->format('U');
+ $displayEvent->detail = __('All Player faults cleared');
+ $displayEvent->save();
+ } else if (empty($currentFaults) && !empty($newPlayerFaults)) {
+ $codesAdded = [];
+ // we do not have any faults currently, but new ones will be added
+ foreach ($newPlayerFaults as $newPlayerFault) {
+ $sanitizedFaultAlert = $this->getSanitizer($newPlayerFault);
+ // if we do not have an alert for the specific code yet, add it
+ if (!in_array($sanitizedFaultAlert->getInt('code'), $codesAdded)) {
+ $this->addDisplayEvent($sanitizedFaultAlert);
+ // keep track of added codes, we want a single alert per code
+ $codesAdded[] = $sanitizedFaultAlert->getInt('code');
+ }
+ }
+ } else if (!empty($newPlayerFaults) && !empty($currentFaults)) {
+ // we have both existing faults and new faults
+ $existingFaultsCodes = [];
+ $newFaultCodes = [];
+ $codesAdded = [];
+
+ // keep track of existing fault codes.
+ foreach ($currentFaults as $currentFault) {
+ $existingFaultsCodes[] = $currentFault->code;
+ }
+
+ // go through new faults
+ foreach ($newPlayerFaults as $newPlayerFault) {
+ $sanitizedFaultAlert = $this->getSanitizer($newPlayerFault);
+ $newFaultCodes[] = $sanitizedFaultAlert->getInt('code');
+ // if it already exists, we do not do anything with alerts
+ // if it is a new code and was not added already
+ // add it now
+ if (!in_array($sanitizedFaultAlert->getInt('code'), $existingFaultsCodes)
+ && !in_array($sanitizedFaultAlert->getInt('code'), $codesAdded)
+ ) {
+ $this->addDisplayEvent($sanitizedFaultAlert);
+ // keep track of added codes, we want a single alert per code
+ $codesAdded[] = $sanitizedFaultAlert->getInt('code');
+ }
+ }
+
+ // go through any existing codes that are no longer reported
+ // update the end date on all of them.
+ foreach (array_diff($existingFaultsCodes, $newFaultCodes) as $code) {
+ $displayEvent = $this->displayEventFactory->createEmpty();
+ $displayEvent->eventEndByReference(
+ $this->display->displayId,
+ $displayEvent->getEventIdFromString('Player Fault'),
+ $code
+ );
+ }
+ }
+ }
+
+ private function addDisplayEvent(SanitizerInterface $sanitizedFaultAlert)
+ {
+ $this->getLog()->debug(
+ sprintf(
+ 'displayEvent : add new display alert for player fault code %d and displayId %d',
+ $sanitizedFaultAlert->getInt('code'),
+ $this->display->displayId
+ )
+ );
+
+ $displayEvent = $this->displayEventFactory->createEmpty();
+ $displayEvent->eventTypeId = $displayEvent->getEventIdFromString('Player Fault');
+ $displayEvent->displayId = $this->display->displayId;
+ $displayEvent->start = $sanitizedFaultAlert->getDate(
+ 'date',
+ ['default' => Carbon::now()]
+ )->format('U');
+ $displayEvent->end = null;
+ $displayEvent->detail = $sanitizedFaultAlert->getString('reason');
+ $displayEvent->refId = $sanitizedFaultAlert->getInt('code');
+ $displayEvent->save();
+ }
}
diff --git a/reports/displayalerts-email-template.twig b/reports/displayalerts-email-template.twig
new file mode 100644
index 0000000000..6f03d91f37
--- /dev/null
+++ b/reports/displayalerts-email-template.twig
@@ -0,0 +1,56 @@
+{#
+/**
+ * Copyright (C) 2024 Xibo Signage Ltd
+ *
+ * Xibo - Digital Signage - https://xibosignage.com
+ *
+ * This file is part of Xibo.
+ *
+ * Xibo is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * Xibo is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Xibo. If not, see
{% trans "Display ID" %} | +{% trans "Display" %} | +{% trans "Event Type" %} | +{% trans "Start" %} | +{% trans "End" %} | +{% trans "Reference ID" %} | +{% trans "Detail" %} | +
---|---|---|---|---|---|---|
{{ item.displayId }} | +{{ item.display }} | +{{ item.eventType }} | +{{ item.start }} | +{{ item.end }} | +{{ item.refId }} | +{{ item.detail }} | +