Skip to content

Commit

Permalink
Merge pull request #79 from roflcoopter/sensor
Browse files Browse the repository at this point in the history
Sensor
  • Loading branch information
roflcoopter authored Nov 12, 2020
2 parents b1add7d + f9a2f55 commit 2ae07aa
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 29 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,24 @@ The folder structure of the faces folder is very strict. Here is an example of t
### Topics for each camera
<div style="margin-left: 1em;">

#### Camera status:
<div style="margin-left: 1em;">
<details>
<summary>Topic: <b><code>{client_id}/{mqtt_name from camera config}/sensor/status/state</b></code></summary>
<div style="margin-left: 1em;">
A JSON formatted payload is published to this topic to indicate the current status of the camera

```json
{"state": "scanning_for_objects", "attributes": {"last_recording_start": <timestamp>, "last_recording_end": <timestamp>}}
```

Possible values in ```state```: ```recording/scanning_for_motion/scanning_for_objects```

</div>
</details>
</details>
</div>

#### Camera control:
<div style="margin-left: 1em;">
<details>
Expand Down Expand Up @@ -965,6 +983,10 @@ A variable amount of cameras will be created based on your configuration.
Images are only sent to this topic if ```publish_image: true```
2) If ```send_to_mqtt``` under ```recorder``` is set to ```true``` , a camera entity named ```camera.{client_id from mqtt config}_{mqtt_name from camera config}_latest_thumbnail``` is created

**Sensor**\
A sensor entity is created for each camera which indicates the status of Viseron.
The state is set to ```recording```, ```scanning_for_motion``` or ```scanning_for_objects``` depending on the situation.

**Binary Sensors**\
A variable amount of binary sensors will be created based on your configuration.
1) A binary sensor showing if any tracked object is in view.
Expand Down
93 changes: 93 additions & 0 deletions src/lib/mqtt/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import json
from lib.helpers import slugify


class MQTTSensor:
def __init__(self, config, mqtt_queue, name):
self._config = config
self._mqtt_queue = mqtt_queue
self._name = f"{config.mqtt.client_id} {config.camera.name} {name}"
self._device_name = f"{config.mqtt.client_id} {config.camera.name}"
self._unique_id = slugify(self._name)
self._node_id = self._config.camera.mqtt_name
self._object_id = slugify(name)

@property
def state_topic(self):
return (
f"{self._config.mqtt.client_id}/{self._node_id}/"
f"sensor/{self.object_id}/state"
)

@property
def config_topic(self):
return (
f"{self._config.mqtt.home_assistant.discovery_prefix}/sensor/"
f"{self.node_id}/{self.object_id}/config"
)

@property
def name(self):
return self._name

@property
def device_name(self):
return self._device_name

@property
def unique_id(self):
return self._unique_id

@property
def node_id(self):
return self._node_id

@property
def object_id(self):
return self._object_id

@property
def device_info(self):
return {
"identifiers": [self.device_name],
"name": self.device_name,
"manufacturer": "Viseron",
}

@property
def config_payload(self):
payload = {}
payload["name"] = self.name # entitu_id
payload["unique_id"] = self.unique_id
payload["state_topic"] = self.state_topic
payload["value_template"] = "{{ value_json.state }}"
payload["availability_topic"] = self._config.mqtt.last_will_topic
payload["payload_available"] = "alive"
payload["payload_not_available"] = "dead"
payload["json_attributes_topic"] = self.state_topic
payload["json_attributes_template"] = "{{ value_json.attributes | tojson }}"
payload["device"] = self.device_info
return json.dumps(payload)

@staticmethod
def state_payload(state, attributes=None):
payload = {}
payload["state"] = state
payload["attributes"] = {}
if attributes:
payload["attributes"] = attributes
return json.dumps(payload)

def on_connect(self, client):
if self._config.mqtt.home_assistant.enable:
client.publish(
self.config_topic, payload=self.config_payload, retain=True,
)

def publish(self, state, attributes=None):
self._mqtt_queue.put(
{
"topic": self.state_topic,
"payload": self.state_payload(state, attributes),
}
)
61 changes: 35 additions & 26 deletions src/lib/nvr.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from lib.mqtt.binary_sensor import MQTTBinarySensor
from lib.mqtt.camera import MQTTCamera
from lib.mqtt.switch import MQTTSwitch
from lib.mqtt.sensor import MQTTSensor
from lib.recorder import FFMPEGRecorder
from lib.zones import Zone

Expand All @@ -31,6 +32,9 @@ def __init__(self, config, mqtt_queue):
self.config = config
self.mqtt_queue = mqtt_queue

self._status_state = None
self.status_attributes = {}

self.devices = {}
if self.mqtt_queue:
self.devices["motion_detected"] = MQTTBinarySensor(
Expand All @@ -45,6 +49,7 @@ def __init__(self, config, mqtt_queue):
)
self.devices["switch"] = MQTTSwitch(config, mqtt_queue)
self.devices["camera"] = MQTTCamera(config, mqtt_queue)
self.devices["sensor"] = MQTTSensor(config, mqtt_queue, "status")

def publish_image(self, object_frame, motion_frame, zones, resolution):
if self.mqtt_queue:
Expand Down Expand Up @@ -75,6 +80,15 @@ def publish_image(self, object_frame, motion_frame, zones, resolution):
if ret:
self.devices["camera"].publish(jpg.tobytes())

@property
def status_state(self):
return self._status_state

@status_state.setter
def status_state(self, state):
self._status_state = state
self.devices["sensor"].publish(state, attributes=self.status_attributes)

def on_connect(self, client):
subscriptions = {}

Expand All @@ -85,32 +99,6 @@ def on_connect(self, client):

return subscriptions

# @property
# def mqtt_sensor_config_payload(self):
# payload = {}
# payload["name"] = self.config.camera.mqtt_name
# payload["state_topic"] = self.mqtt_sensor_state_topic
# payload["value_template"] = "{{ value_json.state }}"
# payload["availability_topic"] = self.config.mqtt.last_will_topic
# payload["payload_available"] = "alive"
# payload["payload_not_available"] = "dead"
# payload["json_attributes_topic"] = self.mqtt_sensor_state_topic
# return json.dumps(payload, indent=3)

# @property
# def mqtt_sensor_state_topic(self):
# return (
# f"{self.config.mqtt.discovery_prefix}/sensor/"
# f"{self.config.camera.mqtt_name}/state"
# )

# @property
# def mqtt_sensor_config_topic(self):
# return (
# f"{self.config.mqtt.discovery_prefix}/sensor/"
# f"{self.config.camera.mqtt_name}/config"
# )


class FFMPEGNVR(Thread):
nvr_list: List[object] = []
Expand Down Expand Up @@ -475,6 +463,26 @@ def process_motion_event(self):
self._logger.debug("Not recording, pausing object detector")
self.camera.scan_for_objects.clear()

def update_status_sensor(self):
status = "unknown"
if self.recorder.is_recording:
status = "recording"
elif self.camera.scan_for_objects.is_set():
status = "scanning_for_objects"
elif self.camera.scan_for_motion.is_set():
status = "scanning_for_motion"

attributes = {}
attributes["last_recording_start"] = self.recorder.last_recording_start
attributes["last_recording_end"] = self.recorder.last_recording_end

if (
status != self._mqtt.status_state
or attributes != self._mqtt.status_attributes
):
self._mqtt.status_attributes = attributes
self._mqtt.status_state = status

def run(self):
""" Main thread. It handles starting/stopping of recordings and
publishes to MQTT if object is detected. Speed is determined by FPS"""
Expand All @@ -484,6 +492,7 @@ def run(self):

self.idle_frames = 0
while not self.kill_received:
self.update_status_sensor()
self.camera.frame_ready.wait()

# Filter returned objects
Expand Down
14 changes: 11 additions & 3 deletions src/lib/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ def __init__(self, config, detection_lock, mqtt_queue):
self._mqtt_queue = mqtt_queue

self.is_recording = False
self.writer_pipe = None
self.last_recording_start = None
self.last_recording_end = None
self._event_start = None
self._event_end = None
self._recording_name = None

segments_folder = os.path.join(
Expand Down Expand Up @@ -85,7 +87,10 @@ def start_recording(self, frame, objects, resolution):
self._logger.info("Starting recorder")
self.is_recording = True
self._segment_cleanup.pause()
self._event_start = int(datetime.datetime.now().timestamp())
now = datetime.datetime.now()
self.last_recording_start = now.isoformat()
self.last_recording_end = None
self._event_start = int(now.timestamp())

if self.config.recorder.folder is None:
self._logger.error("Output directory is not specified")
Expand All @@ -111,7 +116,7 @@ def start_recording(self, frame, objects, resolution):
def concat_segments(self):
self._segmenter.concat_segments(
self._event_start - self.config.recorder.lookback,
int(datetime.datetime.now().timestamp()),
self._event_end,
self._recording_name,
)
# Dont resume cleanup if new recording started during encoding
Expand All @@ -121,5 +126,8 @@ def concat_segments(self):
def stop_recording(self):
self._logger.info("Stopping recorder")
self.is_recording = False
now = datetime.datetime.now()
self.last_recording_end = now.isoformat()
self._event_end = int(now.timestamp())
concat_thread = Thread(target=self.concat_segments)
concat_thread.start()

0 comments on commit 2ae07aa

Please sign in to comment.