aboutsummaryrefslogtreecommitdiff
path: root/bin/sound_sensor_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'bin/sound_sensor_server.py')
-rwxr-xr-xbin/sound_sensor_server.py200
1 files changed, 200 insertions, 0 deletions
diff --git a/bin/sound_sensor_server.py b/bin/sound_sensor_server.py
new file mode 100755
index 0000000..fd7ff5a
--- /dev/null
+++ b/bin/sound_sensor_server.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+import logging
+import threading
+import __py_include
+
+from time import sleep
+from typing import Optional, List, Dict, Tuple
+from functools import partial
+from homekit.config import config
+from homekit.util import Addr
+from homekit.api import WebApiClient, RequestParams
+from homekit.api.types import SoundSensorLocation
+from homekit.soundsensor import SoundSensorServer, SoundSensorHitHandler
+from homekit.media import MediaNodeType, SoundRecordClient, CameraRecordClient, RecordClient
+
+interrupted = False
+logger = logging.getLogger(__name__)
+server: SoundSensorServer
+
+
+def get_related_nodes(node_type: MediaNodeType,
+ sensor_name: str) -> List[str]:
+ try:
+ if sensor_name not in config[f'sensor_to_{node_type.name.lower()}_nodes_relations']:
+ raise ValueError(f'unexpected sensor name {sensor_name}')
+ return config[f'sensor_to_{node_type.name.lower()}_nodes_relations'][sensor_name]
+ except KeyError:
+ return []
+
+
+def get_node_config(node_type: MediaNodeType,
+ name: str) -> Optional[dict]:
+ if name in config[f'{node_type.name.lower()}_nodes']:
+ cfg = config[f'{node_type.name.lower()}_nodes'][name]
+ if 'min_hits' not in cfg:
+ cfg['min_hits'] = 1
+ return cfg
+ else:
+ return None
+
+
+class HitCounter:
+ def __init__(self):
+ self.sensors = {}
+ self.lock = threading.Lock()
+ self._reset_sensors()
+
+ def _reset_sensors(self):
+ for loc in SoundSensorLocation:
+ self.sensors[loc.name.lower()] = 0
+
+ def add(self, name: str, hits: int):
+ if name not in self.sensors:
+ raise ValueError(f'sensor {name} not found')
+
+ with self.lock:
+ self.sensors[name] += hits
+
+ def get_all(self) -> List[Tuple[str, int]]:
+ vals = []
+ with self.lock:
+ for name, hits in self.sensors.items():
+ if hits > 0:
+ vals.append((name, hits))
+ self._reset_sensors()
+ return vals
+
+
+class HitHandler(SoundSensorHitHandler):
+ def handler(self, name: str, hits: int):
+ if not hasattr(SoundSensorLocation, name.upper()):
+ logger.error(f'invalid sensor name: {name}')
+ return
+
+ should_continue = False
+ for node_type in MediaNodeType:
+ try:
+ nodes = get_related_nodes(node_type, name)
+ except ValueError:
+ logger.error(f'config for {node_type.name.lower()} node {name} not found')
+ return
+
+ for node in nodes:
+ node_config = get_node_config(node_type, node)
+ if node_config is None:
+ logger.error(f'config for {node_type.name.lower()} node {node} not found')
+ continue
+ if hits < node_config['min_hits']:
+ continue
+ should_continue = True
+
+ if not should_continue:
+ return
+
+ hc.add(name, hits)
+
+ if not server.is_recording_enabled():
+ return
+ for node_type in MediaNodeType:
+ try:
+ nodes = get_related_nodes(node_type, name)
+ for node in nodes:
+ node_config = get_node_config(node_type, node)
+ if node_config is None:
+ logger.error(f'node config for {node_type.name.lower()} node {node} not found')
+ continue
+
+ durations = node_config['durations']
+ dur = durations[1] if hits > node_config['min_hits'] else durations[0]
+ record_clients[node_type].record(node, dur*60, {'node': node})
+
+ except ValueError as exc:
+ logger.exception(exc)
+
+
+def hits_sender():
+ while not interrupted:
+ all_hits = hc.get_all()
+ if all_hits:
+ api.add_sound_sensor_hits(all_hits)
+ sleep(5)
+
+
+api: Optional[WebApiClient] = None
+hc: Optional[HitCounter] = None
+record_clients: Dict[MediaNodeType, RecordClient] = {}
+
+
+# record callbacks
+# ----------------
+
+def record_error(type: MediaNodeType,
+ info: dict,
+ userdata: dict):
+ node = userdata['node']
+ logger.error('recording ' + str(dict) + f' from {type.name.lower()} node ' + node + ' failed')
+
+ record_clients[type].forget(node, info['id'])
+
+
+def record_finished(type: MediaNodeType,
+ info: dict,
+ fn: str,
+ userdata: dict):
+ logger.debug(f'{type.name.lower()} record finished: ' + str(info))
+
+ # audio could have been requested by other user (telegram bot, for example)
+ # so we shouldn't 'forget' it here
+
+ # node = userdata['node']
+ # record.forget(node, info['id'])
+
+
+# api client callbacks
+# --------------------
+
+def api_error_handler(exc, name, req: RequestParams):
+ logger.error(f'api call ({name}, params={req.params}) failed, exception below')
+ logger.exception(exc)
+
+
+if __name__ == '__main__':
+ config.load_app('sound_sensor_server')
+
+ hc = HitCounter()
+ api = WebApiClient(timeout=(10, 60))
+ api.enable_async(error_handler=api_error_handler)
+
+ t = threading.Thread(target=hits_sender)
+ t.daemon = True
+ t.start()
+
+ sound_nodes = {}
+ if 'sound_nodes' in config:
+ for nodename, nodecfg in config['sound_nodes'].items():
+ sound_nodes[nodename] = Addr.fromstring(nodecfg['addr'])
+
+ camera_nodes = {}
+ if 'camera_nodes' in config:
+ for nodename, nodecfg in config['camera_nodes'].items():
+ camera_nodes[nodename] = Addr.fromstring(nodecfg['addr'])
+
+ if sound_nodes:
+ record_clients[MediaNodeType.SOUND] = SoundRecordClient(sound_nodes,
+ error_handler=partial(record_error, MediaNodeType.SOUND),
+ finished_handler=partial(record_finished, MediaNodeType.SOUND))
+
+ if camera_nodes:
+ record_clients[MediaNodeType.CAMERA] = CameraRecordClient(camera_nodes,
+ error_handler=partial(record_error, MediaNodeType.CAMERA),
+ finished_handler=partial(record_finished, MediaNodeType.CAMERA))
+
+ try:
+ server = SoundSensorServer(config.get_addr('server.listen'), HitHandler)
+ server.run()
+ except KeyboardInterrupt:
+ interrupted = True
+ for c in record_clients.values():
+ c.stop()
+ logging.info('keyboard interrupt, exiting...')