<?php
/**
* @license Silk Software House Sp. Z O.O.
*/
declare(strict_types=1);
namespace App\Entity;
use App\Entity\Traits\IdTrait;
use App\Entity\Traits\TimePreUpdateTrait;
use App\Entity\Traits\TimeTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use DateTime;
use Exception;
/**
* Class Measurement.
*
* @ORM\Table("measurements")
* @ORM\Entity(repositoryClass="App\Repository\MeasurementRepository")
* @ORM\HasLifecycleCallbacks()
*/
class Measurement
{
use IdTrait;
use TimeTrait;
use TimePreUpdateTrait;
public const END_REASON_NORMAL_STOP = 'NORMAL_STOP';
public const END_REASON_UNCLEAN_STOP = 'UNCLEAN_STOP';
/**
* Device.
*
* @var Device|null
*
* @ORM\ManyToOne(targetEntity="App\Entity\Device", cascade={"persist"}, inversedBy="measurements")
* @ORM\JoinColumn(name="device_id", nullable=false)
*/
private ?Device $device = null;
/**
* Customer.
*
* @var Customer|null
*
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
* @ORM\JoinColumn(name="customer_id", nullable=true)
*/
private ?Customer $customer = null;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Channel", mappedBy="measurement")
* @ORM\OrderBy({"number": "ASC"})
*/
private $channels;
/**
* @ORM\Column(type="decimal", precision=5, scale=1)
*/
private ?string $step = null;
/**
* When was this measurement marked as finished (a device sent STOP flag).
*
* @var DateTime|null
*
* @ORM\Column(name="started_at", nullable=true, type="datetime", options={
* "comment": "When was this measurement marked as finished (device sent START flag)."
* })
*/
private ?DateTime $startedAt = null;
/**
* When was this measurement marked as finished (device sent STOP flag).
*
* @var DateTime|null
*
* @ORM\Column(name="finished_at", nullable=true, type="datetime", options={
* "comment": "When was this measurement marked as finished (device sent STOP flag)."
* })
*/
private ?DateTime $finishedAt = null;
/**
* Source period from the device.
*
* @var int|null
*
* @ORM\Column(name="source_period", type="integer", nullable=true, options={"comment": "Source period from device."})
*/
private ?int $sourcePeriod = null;
/**
* The last sample timestamp received for this measurement.
*
* @var DateTime|null
*
* @ORM\Column(name="last_sample_at", nullable=true, type="datetime", options={"comment": "Last sample timestamp received for this measurement."})
*/
private ?DateTime $lastSampleAt = null;
/**
* End reason.
*
* @var string|null
*
* @ORM\Column(name="end_reason", type="string", length=32, nullable=true, options={"comment": "End reason: NORMAL_STOP or UNCLEAN_STOP."})
*/
private ?string $endReason = null;
/**
* Name assigned to this measurement.
*
* @var string|null
*
* @ORM\Column(name="name", type="text", nullable=true, options={"comment": "Name assigned to this measurement."})
*/
private ?string $name = null;
/**
* @ORM\OneToMany(targetEntity="App\Entity\ProjectInformation", mappedBy="measurement", cascade={"persist", "remove"})
*/
private $projectInformation;
/**
* Measurement settings.
*
* @var Collection
*
* @ORM\OneToMany(targetEntity="App\Entity\MeasurementSetting", mappedBy="measurement", cascade={"persist"})
*/
private $settings;
/**
* @var ?string
*/
private ?string $cachedNameWithRelativeNumberPerDevice = null;
/**
* Measurement constructor.
*/
public function __construct()
{
$this->channels = new ArrayCollection();
$this->projectInformation = new ArrayCollection();
$this->settings = new ArrayCollection();
}
/**
* Get device.
*
* @return Device
*/
public function getDevice(): Device
{
if (!isset($this->device)) {
throw new \LogicException('The Device is not set, not expected.');
}
return $this->device;
}
/**
* Set device.
*
* @param Device $device
*
* @return Measurement
*/
public function setDevice(Device $device): self
{
$this->device = $device;
return $this;
}
/**
* Get customer.
*
* @return Customer|null
*/
public function getCustomer(): ?Customer
{
return $this->customer;
}
/**
* Set customer.
*
* @param Customer|null $customer
*
* @return Measurement
*/
public function setCustomer(?Customer $customer): self
{
$this->customer = $customer;
return $this;
}
/**
* @return Collection|Channel[]
*/
public function getChannels(): Collection
{
return $this->channels;
}
/**
* @param Channel $channel
*/
public function addChannel(Channel $channel): void
{
if (!$this->channels->contains($channel)) {
$this->channels->add($channel);
}
}
/**
* @return string|null
*/
public function getStep(): ?string
{
return $this->step;
}
/**
* @param string $step
*
* @return $this
*/
public function setStep(string $step): self
{
$this->step = $step;
return $this;
}
/**
* Get is finished.
*
* @return bool
*/
public function getIsFinished(): bool
{
return null !== $this->finishedAt;
}
/**
* Get name.
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Get name with fallback.
*
* @return string|null
*/
public function getNameWithFallback(): ?string
{
return $this->name ?? ('{{Measurement}} ' . $this->getId());
}
/**
* Get a name with heavy fallback.
*
* This function may be slow.
*
* @return string|null
*/
public function getNameWithHeavyFallback(): ?string
{
if (null === $this->name) {
return $this->getNameWithRelativeNumberPerDevice();
}
return $this->name;
}
/**
* Get a name with a relative number per device.
*
* This function may be slow.
*
* @return string|null
*/
public function getNameWithRelativeNumberPerDevice(): ?string
{
if (!$this->cachedNameWithRelativeNumberPerDevice) {
$measurementIds = $this->getDevice()->getSortedMeasurmentsIdsArray();
if (empty($measurementIds)) {
/* This scenario should never happen, but... */
return '{{Measurement}}';
}
$relativePosition = array_flip($measurementIds)[$this->getId()];
++$relativePosition;
$this->cachedNameWithRelativeNumberPerDevice = '{{Measurement}} ' . $relativePosition;
}
return $this->cachedNameWithRelativeNumberPerDevice;
}
public function getNameWithHeavyFallbackAndDates(): ?string
{
$startAtString = $this->getStartedAt() ? $this->getStartedAt()->format('Y-m-d H:i:s') : '?';
$endsAtString = $this->getFinishedAt() ? $this->getFinishedAt()->format('Y-m-d H:i:s') : 'now';
return $this->getNameWithHeavyFallback().' ('.$startAtString.' - '.$endsAtString.')';
}
/**
* Set name.
*
* @param string|null $name
*
* @return Measurement
*/
public function setName(?string $name): self
{
$this->name = $name;
return $this;
}
/**
* Get started at.
*
* @return DateTime|null
*/
public function getStartedAt(): ?DateTime
{
return $this->startedAt;
}
/**
* Set started at.
*
* @param DateTime|null $startedAt
*
* @return Measurement
*/
public function setStartedAt(?DateTime $startedAt): self
{
$this->startedAt = $startedAt;
return $this;
}
/**
* Get started at string.
*
* @param string|null $default
*
* @return string|null
*/
public function getStartedAtString(?string $default = null): ?string
{
return $this->startedAt ? $this->startedAt->format('Y-m-d H:i:s') : $default;
}
/**
* Get finished at.
*
* @return DateTime|null
*/
public function getFinishedAt(): ?DateTime
{
return $this->finishedAt;
}
/**
* Set finished at.
*
* @param DateTime|null $finishedAt
*
* @return Measurement
*/
public function setFinishedAt(?DateTime $finishedAt): self
{
$this->finishedAt = $finishedAt;
return $this;
}
/**
* @return int|null
*/
public function getSourcePeriod(): ?int
{
return $this->sourcePeriod;
}
/**
* @param int|null $sourcePeriod
*
* @return Measurement
*/
public function setSourcePeriod(?int $sourcePeriod): self
{
$this->sourcePeriod = $sourcePeriod;
return $this;
}
/**
* @return DateTime|null
*/
public function getLastSampleAt(): ?DateTime
{
return $this->lastSampleAt;
}
/**
* @param DateTime|null $lastSampleAt
*
* @return Measurement
*/
public function setLastSampleAt(?DateTime $lastSampleAt): self
{
$this->lastSampleAt = $lastSampleAt;
return $this;
}
/**
* Returns the latest timestamp actually received from the device.
*
* Falls back to probe data for older/imported measurements where lastSampleAt
* was not populated yet.
*/
public function getLastReceivedAt(): ?DateTime
{
if ($this->lastSampleAt) {
return $this->lastSampleAt;
}
$lastProbeAt = null;
foreach ($this->channels as $channel) {
$probe = $channel->getProbes()->last();
if (!$probe) {
continue;
}
if (null === $lastProbeAt || $probe->getTime() > $lastProbeAt) {
$lastProbeAt = $probe->getTime();
}
}
return $lastProbeAt ?: $this->finishedAt;
}
/**
* @return string|null
*/
public function getEndReason(): ?string
{
return $this->endReason;
}
/**
* @param string|null $endReason
*
* @return Measurement
*/
public function setEndReason(?string $endReason): self
{
$this->endReason = $endReason;
return $this;
}
/**
* Get finished at string.
*
* @param string|null $default
*
* @return string|null
*/
public function getFinishedAtString(?string $default = null): ?string
{
return $this->finishedAt ? $this->finishedAt->format('Y-m-d H:i:s') : $default;
}
/**
* @return Collection|ProjectInformation[]
*/
public function getProjectInformation(): Collection
{
return $this->projectInformation;
}
/**
* Add project information.
*
* @param ProjectInformation $projectInformation
*
* @return Measurement
*/
public function addProjectInformation(ProjectInformation $projectInformation): self
{
if (!$this->projectInformation->contains($projectInformation)) {
$this->projectInformation->add($projectInformation->setMeasurement($this));
}
return $this;
}
/**
* Remove project information.
*
* @param ProjectInformation $projectInformation
*
* @return Measurement
*/
public function removeProjectInformation(ProjectInformation $projectInformation): self
{
if ($this->projectInformation->contains($projectInformation)) {
$this->projectInformation->removeElement($projectInformation);
// set the owning side to null (unless already changed)
if ($projectInformation->getMeasurement()->getId() === $this->getId()) {
$projectInformation->setMeasurement(null);
}
}
return $this;
}
/**
* @return false|int
*
* @throws Exception
*/
public function getActiveDays()
{
$until = $this->getFinishedAt() ?? new DateTime();
$diff = date_diff($this->getStartedAt() ?? $this->createdAt, $until);
return $diff->days;
}
/**
* Get a standard recipe.
*
* @return Recipe|null
*/
public function getStandardRecipe(): ?Recipe
{
return $this->getSetting() ? $this->getSetting()->getStandardRecipe() : null;
}
/**
* Set a standard recipe.
*
* @param Recipe|null $standardRecipe
*
* @return Measurement
*/
public function setStandardRecipe(?Recipe $standardRecipe): self
{
$this->getSetting()->setStandardRecipe($standardRecipe);
return $this;
}
/**
* Get setting.
*
* @return MeasurementSetting|null
*/
public function getSetting(): ?MeasurementSetting
{
return $this->settings->first() ?: null;
}
/**
* To string.
*
* @return string
*/
public function __toString(): string
{
$startAtString = $this->getStartedAt() ? $this->getStartedAt()->format('Y-m-d H:i:s') : '?';
$endsAtString = $this->getFinishedAt() ? $this->getFinishedAt()->format('Y-m-d H:i:s') : 'now';
return $this->getNameWithFallback().' ('.$startAtString.' - '.$endsAtString.')';
}
/**
* @return string
*/
public function getChannelDate(): string
{
$startAtString = $this->getStartedAt() ? $this->getStartedAt()->format('Y-m-d H:i:s') : '?';
$endsAtString = $this->getFinishedAt() ? $this->getFinishedAt()->format('Y-m-d H:i:s') : 'now';
return ' ('.$startAtString.' - '.$endsAtString.')';
}
/**
* Get nth channel.
*
* @param int $index
*
* @return ChannelSetting|null
*/
public function getNthChannelSetting(int $index): ?ChannelSetting
{
if (!$this->getSetting()) {
return null;
}
return $this->getSetting()->getChannelSettings()[$index] ?? null;
}
/**
* Has channel limit exceeded.
*
* @return bool
*/
public function hasChannelsLimitExceeded(): bool
{
return null !== $this->getChannelsLeft() && $this->getChannelsLeft() <= 0;
}
/**
* Channels left.
*
* @return int|null
*/
public function getChannelsLeft(): ?int
{
if (!$this->device || !$this->device->getChannelsLimit()) {
return null;
}
if (!$this->settings->count()) {
return null;
}
return $this->device->getChannelsLimit() - $this->settings->first()->getChannelSettings()->count();
}
/**
* Add setting.
*
* @param MeasurementSetting $setting
*
* @return Measurement
*/
public function setSetting(MeasurementSetting $setting): self
{
if (0 === $this->settings->count()) {
$this->settings->add($setting);
}
return $this;
}
/**
* Get line type group.
*
* @return LineTypeGroup|null
*/
public function getLineTypeGroup(): ?LineTypeGroup
{
return $this->getSetting() ? $this->getSetting()->getLineTypeGroup() : null;
}
/**
* Set a line type group.
*
* @param LineTypeGroup|null $lineTypeGroup
*
* @return Measurement
*/
public function setLineTypeGroup(?LineTypeGroup $lineTypeGroup): self
{
if ($this->getSetting()) {
$this->getSetting()->setLineTypeGroup($lineTypeGroup);
}
return $this;
}
}