changeset 2:f822e7fe7120

rewrite to use telemetrix (localized)
author drewp@bigasterisk.com
date Sun, 05 Feb 2023 14:06:19 -0800
parents 125c794511a6
children 67402d8b4e0d
files doorbell_to_mqtt.py pdm.lock pyproject.toml
diffstat 3 files changed, 216 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/doorbell_to_mqtt.py	Sun Feb 05 14:05:06 2023 -0800
+++ b/doorbell_to_mqtt.py	Sun Feb 05 14:06:19 2023 -0800
@@ -1,60 +1,97 @@
-import time
+import asyncio
 import logging
 import sys
-import paho.mqtt.client as mqtt
-from pyfirmata import Arduino, INPUT
 
-PULLUP = 0x0b
+import asyncio_mqtt
+import uvicorn
+from starlette.applications import Starlette
+from starlette.requests import Request
+from starlette.responses import HTMLResponse, RedirectResponse
+from starlette.routing import Route
+from typing import Optional
+import telemetrix_local as telemetrix_aio
 
-MIN_REPEAT_SEC = .5
+DIGITAL_PIN = 2  # arduino pin number
+KILL_TIME = 5  # sleep time to keep forever loop open
+MQTT_RECONNECT_SEC = 5
+MIN_REPEAT_SEC = 2
 
-logging.basicConfig(level=logging.INFO)
+logging.basicConfig(level=logging.DEBUG)
 log = logging.getLogger()
 
 dev, = sys.argv[1:]
 
-client = mqtt.Client()
-client.will_set('doorbell/online', '0', qos=0, retain=False)
-client.connect('bang')
 
-log.info(f'connecting to {dev}')
-# note that I set StandardFirmata to double baud rate
-board = Arduino(dev, baudrate=115200)
-log.info('done')
+async def digital_in_pullup(my_board, pin):
+    last_report = 0
 
-pin = board.get_pin('d:2:i')
-pin.mode = PULLUP  # config msg to send to firmata
-pin._mode = INPUT  # fake value so pyfirmata still reads this pin
+    async def cb(data):
+        nonlocal last_report
+        _pinmode, _pin, value, t = data
+        pressed = not value
+        if pressed:
+            if last_report < t - MIN_REPEAT_SEC:
+                log.info(f'press {t}')
+                await press()
+                last_report = t
 
-log.info('running mqtt client')
-client.loop_start()
-log.info('done')
-
-client.publish('doorbell/online', '1')
-
-last_value = None
-last_edge = 0
+    await my_board.set_pin_mode_digital_input_pullup(pin, cb)
+    await board.enable_digital_reporting(2)
+    while True:
+        await asyncio.sleep(KILL_TIME)
 
 
-def on_press(t):
-    global last_edge
-    if t > last_edge + MIN_REPEAT_SEC:
-        log.info(f'press {t=}')
-        client.publish('doorbell/button', 'press')
-        log.info('published')
-    last_edge = t
+board = telemetrix_aio.TelemetrixAIO(com_port=dev, autostart=False)
+
+cli: Optional[asyncio_mqtt.Client] =None
+
+async def press():
+    await cli.publish('doorbell/button', 'pressed')
+
+async def status(request: Request) -> HTMLResponse:
+    return HTMLResponse("""<doctype HTML>
+    <html>
+    <head><title>doorbell</title></head>
+    <body>
+    <form method="POST" action="press">
+      <button type="submit">test ring</button>
+    </form>
+    </body>
+    </html>
+    """)
+
+async def post_press(request: Request) -> RedirectResponse:
+    await press()
+    return RedirectResponse('.', status_code=303)
+
 
 
-try:
-    while True:
-        now = time.time()
-        board.iterate()
-        value = pin.read()
-        if value == 0 and last_value == 1:
-            on_press(now)
-        last_value = value
-        client.loop()
-except Exception:
-    client.publish('doorbell/online', '0')
-    client.disconnect()
-    raise
\ No newline at end of file
+app = Starlette(routes=[
+    Route('/', status),
+    Route('/press', post_press, methods=['POST']),
+])
+
+
+async def main():
+    global cli
+    config = uvicorn.Config(app, host='0.0.0.0', port=8000, log_level="info")
+    server = uvicorn.Server(config)
+
+    online = 'doorbell/online'
+    will = asyncio_mqtt.Will(topic=online, payload='0')
+    async with asyncio_mqtt.Client(
+            "bang",
+            keepalive=10,  # don't go below firmata startup time of 5sec
+            will=will) as client:
+        log.debug(f'with {client=}')
+        cli=client
+        await board.start_aio()
+
+        t1 = asyncio.create_task(server.serve())
+        t2 = asyncio.create_task(digital_in_pullup(board, 2))
+        await client.publish(online, '1')
+        while True:
+            await asyncio.sleep(KILL_TIME)
+
+
+asyncio.run(main(), debug=True)
\ No newline at end of file
--- a/pdm.lock	Sun Feb 05 14:05:06 2023 -0800
+++ b/pdm.lock	Sun Feb 05 14:06:19 2023 -0800
@@ -1,34 +1,153 @@
+[[package]]
+name = "anyio"
+version = "3.6.2"
+requires_python = ">=3.6.2"
+summary = "High level compatibility layer for multiple asynchronous event loop implementations"
+dependencies = [
+    "idna>=2.8",
+    "sniffio>=1.1",
+]
+
+[[package]]
+name = "asyncio-mqtt"
+version = "0.16.1"
+requires_python = ">=3.7"
+summary = "Idiomatic asyncio wrapper around paho-mqtt"
+dependencies = [
+    "paho-mqtt>=1.6.0",
+]
+
+[[package]]
+name = "click"
+version = "8.1.3"
+requires_python = ">=3.7"
+summary = "Composable command line interface toolkit"
+dependencies = [
+    "colorama; platform_system == \"Windows\"",
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+summary = "Cross-platform colored terminal text."
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+requires_python = ">=3.7"
+summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+
+[[package]]
+name = "idna"
+version = "3.4"
+requires_python = ">=3.5"
+summary = "Internationalized Domain Names in Applications (IDNA)"
+
 [[package]]
 name = "paho-mqtt"
 version = "1.6.1"
 summary = "MQTT version 5.0/3.1.1 client class"
 
 [[package]]
-name = "pyfirmata"
-version = "1.1.0"
-summary = "A Python interface for the Firmata procotol"
+name = "pyserial"
+version = "3.5"
+summary = "Python Serial Port Extension"
+
+[[package]]
+name = "pyserial-asyncio"
+version = "0.6"
+summary = "Python Serial Port Extension - Asynchronous I/O support"
+dependencies = [
+    "pyserial",
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+requires_python = ">=3.7"
+summary = "Sniff out which async library your code is running under"
+
+[[package]]
+name = "starlette"
+version = "0.23.1"
+requires_python = ">=3.7"
+summary = "The little ASGI library that shines."
+dependencies = [
+    "anyio<5,>=3.4.0",
+]
+
+[[package]]
+name = "telemetrix-aio"
+version = "1.11"
+summary = "Remotely Control And Monitor Arduino-Core devices"
 dependencies = [
     "pyserial",
 ]
 
 [[package]]
-name = "pyserial"
-version = "3.5"
-summary = "Python Serial Port Extension"
+name = "uvicorn"
+version = "0.20.0"
+requires_python = ">=3.7"
+summary = "The lightning-fast ASGI server."
+dependencies = [
+    "click>=7.0",
+    "h11>=0.8",
+]
 
 [metadata]
 lock_version = "4.0"
-content_hash = "sha256:0a4f0629cf61e777ba036a8c5120073441e264d2acd07f19e4bae0816a7521ff"
+content_hash = "sha256:6d9a6594c6d7a72bfc8de7484e6176d33e1306564ca0de1d697c83601cd12ca1"
 
 [metadata.files]
+"anyio 3.6.2" = [
+    {url = "https://files.pythonhosted.org/packages/77/2b/b4c0b7a3f3d61adb1a1e0b78f90a94e2b6162a043880704b7437ef297cad/anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
+    {url = "https://files.pythonhosted.org/packages/8b/94/6928d4345f2bc1beecbff03325cad43d320717f51ab74ab5a571324f4f5a/anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
+]
+"asyncio-mqtt 0.16.1" = [
+    {url = "https://files.pythonhosted.org/packages/28/0f/008e00d479dc7c666e8cef37d8f738e330a525d6f01fcd351d7fce71681b/asyncio_mqtt-0.16.1-py3-none-any.whl", hash = "sha256:ef12c1d0250ffd65b70085d66c457128ed6aa98b3d821d2316213fb9d1957d5b"},
+    {url = "https://files.pythonhosted.org/packages/b9/ae/bb75e07de548e883ef0b813f139613f13ac43e79874b1365af21572a7eaa/asyncio_mqtt-0.16.1.tar.gz", hash = "sha256:38416481bb7cc9613c6a1a8e58b8f9f9c9a522d93411003e8000e74ef2b7ed85"},
+]
+"click 8.1.3" = [
+    {url = "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+    {url = "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+]
+"colorama 0.4.6" = [
+    {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+"h11 0.14.0" = [
+    {url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+    {url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+"idna 3.4" = [
+    {url = "https://files.pythonhosted.org/packages/8b/e1/43beb3d38dba6cb420cefa297822eac205a277ab43e5ba5d5c46faf96438/idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+    {url = "https://files.pythonhosted.org/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+]
 "paho-mqtt 1.6.1" = [
     {url = "https://files.pythonhosted.org/packages/f8/dd/4b75dcba025f8647bc9862ac17299e0d7d12d3beadbf026d8c8d74215c12/paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f"},
 ]
-"pyfirmata 1.1.0" = [
-    {url = "https://files.pythonhosted.org/packages/14/f0/05e30c9cee38f9c0e8ef08a07ff28e4c391c7d95e2bcb1f334f58cc2bb28/pyFirmata-1.1.0-py2.py3-none-any.whl", hash = "sha256:03091ffd0b06a483d286d9738ead97a50c55cf1bf6b89a5bd0c420d0a7797420"},
-    {url = "https://files.pythonhosted.org/packages/ff/e6/512cc154c0370e22429db3139e7c906c4f8ad2646f4ca816547e9ae2c26c/pyFirmata-1.1.0.tar.gz", hash = "sha256:cc180d1b30c85a2bbca62c15fef1b871db048cdcfa80959968356d97bd3ff08e"},
-]
 "pyserial 3.5" = [
     {url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"},
     {url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"},
 ]
+"pyserial-asyncio 0.6" = [
+    {url = "https://files.pythonhosted.org/packages/27/24/c820cf15f87f7b164e83710c1852d4f900d9793961579e5ef64189bc0c10/pyserial_asyncio-0.6-py3-none-any.whl", hash = "sha256:de9337922619421b62b9b1a84048634b3ac520e1d690a674ed246a2af7ce1fc5"},
+    {url = "https://files.pythonhosted.org/packages/4a/9a/8477699dcbc1882ea51dcff4d3c25aa3f2063ed8f7d7a849fd8f610506b6/pyserial-asyncio-0.6.tar.gz", hash = "sha256:b6032923e05e9d75ec17a5af9a98429c46d2839adfaf80604d52e0faacd7a32f"},
+]
+"sniffio 1.3.0" = [
+    {url = "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
+    {url = "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+]
+"starlette 0.23.1" = [
+    {url = "https://files.pythonhosted.org/packages/0c/d2/a2898deb36d12e40e84e83b7728628a04013cb0cfc545932c4605185bf2d/starlette-0.23.1.tar.gz", hash = "sha256:8510e5b3d670326326c5c1d4cb657cc66832193fe5d5b7015a51c7b1e1b1bf42"},
+    {url = "https://files.pythonhosted.org/packages/a3/1d/b23984c05e39ddab35bbba33a3828dc4f896250220dcbd946c0fcad1e934/starlette-0.23.1-py3-none-any.whl", hash = "sha256:ec69736c90be8dbfc6ec6800ba6feb79c8c44f9b1706c0b2bb27f936bcf362cc"},
+]
+"telemetrix-aio 1.11" = [
+    {url = "https://files.pythonhosted.org/packages/8f/49/6df494d48bde51bc493de307c019ddbe2a95daa36e39c17f5985329c4170/telemetrix_aio-1.11-py2.py3-none-any.whl", hash = "sha256:815211f0320841c594e9ff623c455bbdc10656a8ffb45112c8de012a2b7e65cc"},
+    {url = "https://files.pythonhosted.org/packages/f9/f2/c4baf39f4257145abdbaf2f12d8e9f96f64c3d604aca1603773b14786c6f/telemetrix-aio-1.11.tar.gz", hash = "sha256:2f1afe3293b580d0744f7d72ab8129cc3f457d08b5bb19740b18a178d9e14035"},
+]
+"uvicorn 0.20.0" = [
+    {url = "https://files.pythonhosted.org/packages/95/3c/9f4650ff609370456f796bb96a355dcddef1ded67e05d1b4eb3481088329/uvicorn-0.20.0.tar.gz", hash = "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8"},
+    {url = "https://files.pythonhosted.org/packages/96/f3/f39ac8ac3bdf356b4934b8f7e56173e96681f67ef0cd92bd33a5059fae9e/uvicorn-0.20.0-py3-none-any.whl", hash = "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd"},
+]
--- a/pyproject.toml	Sun Feb 05 14:05:06 2023 -0800
+++ b/pyproject.toml	Sun Feb 05 14:06:19 2023 -0800
@@ -6,8 +6,12 @@
     {name = "Drew Perttula", email = "drewp@bigasterisk.com"},
 ]
 dependencies = [
-    "pyfirmata>=1.1.0",
+    "asyncio-mqtt>=0.16.1",
     "paho-mqtt>=1.6.1",
+    "pyserial-asyncio>=0.6",
+    "starlette>=0.23.1",
+    "telemetrix-aio>=1.11",
+    "uvicorn>=0.20.0",
 ]
 requires-python = ">=3.10"
 license = {text = "MIT"}