summaryrefslogtreecommitdiff
path: root/src/home/soundsensor
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2021-11-27 16:17:05 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-04-24 01:33:04 +0300
commitc412bf2ee0a3fbf9032fc32a26837d4fbc7585c5 (patch)
tree5cca6bcab79331ad82cab4219c7692b9dd4eea21 /src/home/soundsensor
initial public
Diffstat (limited to 'src/home/soundsensor')
-rw-r--r--src/home/soundsensor/__init__.py22
-rw-r--r--src/home/soundsensor/__init__.pyi8
-rw-r--r--src/home/soundsensor/node.py73
-rw-r--r--src/home/soundsensor/server.py125
-rw-r--r--src/home/soundsensor/server_client.py38
5 files changed, 266 insertions, 0 deletions
diff --git a/src/home/soundsensor/__init__.py b/src/home/soundsensor/__init__.py
new file mode 100644
index 0000000..30052f8
--- /dev/null
+++ b/src/home/soundsensor/__init__.py
@@ -0,0 +1,22 @@
+import importlib
+
+__all__ = [
+ 'SoundSensorNode',
+ 'SoundSensorHitHandler',
+ 'SoundSensorServer',
+ 'SoundSensorServerGuardClient'
+]
+
+
+def __getattr__(name):
+ if name in __all__:
+ if name == 'SoundSensorNode':
+ file = 'node'
+ elif name == 'SoundSensorServerGuardClient':
+ file = 'server_client'
+ else:
+ file = 'server'
+ module = importlib.import_module(f'.{file}', __name__)
+ return getattr(module, name)
+
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/home/soundsensor/__init__.pyi b/src/home/soundsensor/__init__.pyi
new file mode 100644
index 0000000..cb34972
--- /dev/null
+++ b/src/home/soundsensor/__init__.pyi
@@ -0,0 +1,8 @@
+from .server import (
+ SoundSensorHitHandler as SoundSensorHitHandler,
+ SoundSensorServer as SoundSensorServer,
+)
+from .server_client import (
+ SoundSensorServerGuardClient as SoundSensorServerGuardClient
+)
+from .node import SoundSensorNode as SoundSensorNode
diff --git a/src/home/soundsensor/node.py b/src/home/soundsensor/node.py
new file mode 100644
index 0000000..b4b8fbc
--- /dev/null
+++ b/src/home/soundsensor/node.py
@@ -0,0 +1,73 @@
+import logging
+import threading
+
+from typing import Optional
+from time import sleep
+from ..util import stringify, send_datagram, Addr
+
+from pyA20.gpio import gpio
+from pyA20.gpio import port as gpioport
+
+logger = logging.getLogger(__name__)
+
+
+class SoundSensorNode:
+ def __init__(self,
+ name: str,
+ pinname: str,
+ server_addr: Optional[Addr],
+ delay=0.005):
+
+ if not hasattr(gpioport, pinname):
+ raise ValueError(f'invalid pin {pinname}')
+
+ self.pin = getattr(gpioport, pinname)
+ self.name = name
+ self.delay = delay
+
+ self.server_addr = server_addr
+
+ self.hits = 0
+ self.hitlock = threading.Lock()
+
+ self.interrupted = False
+
+ def run(self):
+ try:
+ t = threading.Thread(target=self.sensor_reader)
+ t.daemon = True
+ t.start()
+
+ while True:
+ with self.hitlock:
+ hits = self.hits
+ self.hits = 0
+
+ if hits > 0:
+ try:
+ if self.server_addr is not None:
+ send_datagram(stringify([self.name, hits]), self.server_addr)
+ else:
+ logger.debug(f'server reporting disabled, skipping reporting {hits} hits')
+ except OSError as exc:
+ logger.exception(exc)
+
+ sleep(1)
+
+ except (KeyboardInterrupt, SystemExit) as e:
+ self.interrupted = True
+ logger.info(str(e))
+
+ def sensor_reader(self):
+ gpio.init()
+ gpio.setcfg(self.pin, gpio.INPUT)
+ gpio.pullup(self.pin, gpio.PULLUP)
+
+ while not self.interrupted:
+ state = gpio.input(self.pin)
+ sleep(self.delay)
+
+ if not state:
+ with self.hitlock:
+ logger.debug('got a hit')
+ self.hits += 1
diff --git a/src/home/soundsensor/server.py b/src/home/soundsensor/server.py
new file mode 100644
index 0000000..490fc36
--- /dev/null
+++ b/src/home/soundsensor/server.py
@@ -0,0 +1,125 @@
+import asyncio
+import json
+import logging
+import threading
+
+from ..config import config
+from aiohttp import web
+from aiohttp.web_exceptions import (
+ HTTPNotFound
+)
+
+from typing import Type
+from ..util import Addr, stringify, format_tb
+
+logger = logging.getLogger(__name__)
+
+
+class SoundSensorHitHandler(asyncio.DatagramProtocol):
+ def datagram_received(self, data, addr):
+ try:
+ data = json.loads(data)
+ except json.JSONDecodeError as e:
+ logger.error('failed to parse json datagram')
+ logger.exception(e)
+ return
+
+ try:
+ name, hits = data
+ except (ValueError, IndexError) as e:
+ logger.error('failed to unpack data')
+ logger.exception(e)
+ return
+
+ self.handler(name, hits)
+
+ def handler(self, name: str, hits: int):
+ pass
+
+
+class SoundSensorServer:
+ def __init__(self,
+ addr: Addr,
+ handler_impl: Type[SoundSensorHitHandler]):
+ self.addr = addr
+ self.impl = handler_impl
+
+ self._recording_lock = threading.Lock()
+ self._recording_enabled = True
+
+ if self.guard_control_enabled():
+ if 'guard_recording_default' in config['server']:
+ self._recording_enabled = config['server']['guard_recording_default']
+
+ def guard_control_enabled(self) -> bool:
+ return 'guard_control' in config['server'] and config['server']['guard_control'] is True
+
+ def set_recording(self, enabled: bool):
+ with self._recording_lock:
+ self._recording_enabled = enabled
+
+ def is_recording_enabled(self) -> bool:
+ with self._recording_lock:
+ return self._recording_enabled
+
+ def run(self):
+ if self.guard_control_enabled():
+ t = threading.Thread(target=self.run_guard_server)
+ t.daemon = True
+ t.start()
+
+ loop = asyncio.get_event_loop()
+ t = loop.create_datagram_endpoint(self.impl, local_addr=self.addr)
+ loop.run_until_complete(t)
+ loop.run_forever()
+
+ def run_guard_server(self):
+ routes = web.RouteTableDef()
+
+ def ok(data=None):
+ if data is None:
+ data = 1
+ response = {'response': data}
+ return web.json_response(response, dumps=stringify)
+
+ @web.middleware
+ async def errors_handler_middleware(request, handler):
+ try:
+ response = await handler(request)
+ return response
+ except HTTPNotFound:
+ return web.json_response({'error': 'not found'}, status=404)
+ except Exception as exc:
+ data = {
+ 'error': exc.__class__.__name__,
+ 'message': exc.message if hasattr(exc, 'message') else str(exc)
+ }
+ tb = format_tb(exc)
+ if tb:
+ data['stacktrace'] = tb
+
+ return web.json_response(data, status=500)
+
+ @routes.post('/guard/enable')
+ async def guard_enable(request):
+ self.set_recording(True)
+ return ok()
+
+ @routes.post('/guard/disable')
+ async def guard_disable(request):
+ self.set_recording(False)
+ return ok()
+
+ @routes.get('/guard/status')
+ async def guard_status(request):
+ return ok({'enabled': self.is_recording_enabled()})
+
+ asyncio.set_event_loop(asyncio.new_event_loop()) # need to create new event loop in new thread
+ app = web.Application()
+ app.add_routes(routes)
+ app.middlewares.append(errors_handler_middleware)
+
+ web.run_app(app,
+ host=self.addr[0],
+ port=self.addr[1],
+ handle_signals=False) # handle_signals=True doesn't work in separate thread
diff --git a/src/home/soundsensor/server_client.py b/src/home/soundsensor/server_client.py
new file mode 100644
index 0000000..7eef996
--- /dev/null
+++ b/src/home/soundsensor/server_client.py
@@ -0,0 +1,38 @@
+import requests
+import logging
+
+from ..util import Addr
+from ..api.errors import ApiResponseError
+
+
+class SoundSensorServerGuardClient:
+ def __init__(self, addr: Addr):
+ self.endpoint = f'http://{addr[0]}:{addr[1]}'
+ self.logger = logging.getLogger(self.__class__.__name__)
+
+ def guard_enable(self):
+ return self._call('guard/enable', is_post=True)
+
+ def guard_disable(self):
+ return self._call('guard/disable', is_post=True)
+
+ def guard_status(self):
+ return self._call('guard/status')
+
+ def _call(self,
+ method: str,
+ is_post=False):
+
+ url = f'{self.endpoint}/{method}'
+ self.logger.debug(f'calling {url}')
+
+ r = requests.get(url) if not is_post else requests.post(url)
+
+ if r.status_code != 200:
+ response = r.json()
+ raise ApiResponseError(status_code=r.status_code,
+ error_type=response['error'],
+ error_message=response['message'] or None,
+ error_stacktrace=response['stacktrace'] if 'stacktrace' in response else None)
+
+ return r.json()['response']