Add default to item_older_than_n_days.
[kiosk.git] / myq_renderer.py
index a4c29ff5160100ee24994f21dcf02596dedbcbc4..ebfac233927a8038cda3daca83a2fe23fb984d78 100644 (file)
-#!/usr/local/bin/python
+#!/usr/bin/env python3
 
-import requests
-import os
-import json
-import time
-import constants
+from aiohttp import ClientSession
+import asyncio
 import datetime
-import file_writer
-import globals
-import httplib
-import json
-import renderer
-import secrets
-
-APP_ID = "Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB/i"
-HOST_URI = "myqexternal.myqdevice.com"
-LOGIN_ENDPOINT = "api/v4/User/Validate"
-DEVICE_LIST_ENDPOINT = "api/v4/UserDeviceDetails/Get"
-DEVICE_SET_ENDPOINT = "api/v4/DeviceAttribute/PutDeviceAttribute"
-
-class MyQDoor:
-    myq_device_id = None
-    myq_lamp_device_id = None
-    myq_device_descr = None
-    myq_device_update_ts = None
-    myq_device_state = None
-    myq_security_token = None
-
-    def __init__(self, device_id, lamp_id, descr, update_ts, state, token):
-        self.myq_device_id = device_id
-        self.myq_lamp_device_id = lamp_id
-        self.myq_device_descr = descr
-        self.myq_device_update_ts = update_ts
-        self.myq_device_state = state
-        self.myq_security_token = token
-
-    def update(self, update_ts, state):
-        self.myq_device_update_ts = update_ts
-        self.myq_device_state = state
+from dateutil.parser import parse
+import pymyq  # type: ignore
+from typing import Dict, Optional
 
-    def get_name(self):
-        return self.myq_device_descr
+from pyutils.datetimes import datetime_utils
 
-    def get_update_ts(self):
-        return self.myq_device_update_ts
-
-    def open(self):
-        self.change_door_state("open")
-
-    def close(self):
-        self.change_door_state("close")
+import kiosk_constants
+import file_writer
+import renderer
+import kiosk_secrets as secrets
 
-    def lamp_on(self):
-        self.change_lamp_state("on")
 
-    def lamp_off(self):
-        self.change_lamp_state("off")
+class garage_door_renderer(renderer.abstaining_renderer):
+    def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None:
+        super().__init__(name_to_timeout_dict)
+        self.doors: Optional[Dict] = None
+        self.last_update: Optional[datetime.datetime] = None
 
-    def get_status(self):
-        state = self.myq_device_state
-        if state == "1":
-            return "open"
-        elif state == "2":
-            return "closed"
-        elif state == "3":
-            return "stopped"
-        elif state == "4":
-            return "opening"
-        elif state == "5":
-            return "closing"
-        elif state == "8":
-            return "moving"
-        elif state == "9":
-            return "open"
-        else:
-            return str(state) + ", an unknown state for the door."
-
-    def get_state_icon(self):
-        state = self.myq_device_state
-        if (state == "1" or state == "9"):
-            return "/icons/garage_open.png"
-        elif (state == "2"):
-            return "/icons/garage_closed.png"
-        elif (state == "4"):
-            return "/icons/garage_opening.png"
-        elif (state == "5" or state == "8"):
-            return "/icons/garage_closing.png"
-        else:
-            return str(state) + ", an unknown state for the door."
+    def debug_prefix(self) -> str:
+        return "myq"
 
-    def get_lamp_status(self):
-        state = self.check_lamp_state()
-        if state == "0":
-            return "off"
-        elif state == "1":
-            return "on"
+    def periodic_render(self, key: str) -> bool:
+        if key == "Poll MyQ":
+            self.last_update = datetime.datetime.now()
+            return asyncio.run(self.poll_myq())
+        elif key == "Update Page":
+            return self.update_page()
         else:
-            return "unknown"
-
-    def change_device_state(self, payload):
-        device_action = requests.put(
-            'https://{host_uri}/{device_set_endpoint}'.format(
-                host_uri=HOST_URI,
-                device_set_endpoint=DEVICE_SET_ENDPOINT),
-                data=payload,
-                headers={
-                    'MyQApplicationId': APP_ID,
-                    'SecurityToken': self.myq_security_token
-                }
-        )
-        return device_action.status_code == 200
-
-    def change_lamp_state(self, command):
-        newstate = 1 if command.lower() == "on" else 0
-        payload = {
-            "attributeName": "desiredlightstate",
-            "myQDeviceId": self.myq_lamp_device_id,
-            "AttributeValue": newstate
-        }
-        return self.change_device_state(payload)
-
-    def change_door_state(self, command):
-        open_close_state = 1 if command.lower() == "open" else 0
-        payload = {
-            'attributeName': 'desireddoorstate',
-            'myQDeviceId': self.myq_device_id,
-            'AttributeValue': open_close_state,
-        }
-        return self.change_device_state(payload)
-
-    def check_door_state(self):
-        return self.myq_device_state
+            raise Exception("Unknown operaiton")
 
-#    def check_lamp_state(self):
-#        return self.check_device_state(self.myq_lamp_device_id, "lightstate")
-
-    def __repr__(self):
-        return "MyQ device(%s/%s), last update %s, current state %s" % (
-            self.myq_device_descr,
-            self.myq_device_id,
-            self.myq_device_update_ts,
-            self.get_status());
-
-class MyQService:
-    myq_security_token = ""
-    myq_device_list = {}
-
-    def __init__(self, username, password):
-        self.username = username
-        self.password = password
-
-    def login(self):
-        params = {
-            'username': self.username,
-            'password': self.password
-        }
-        login = requests.post(
-                'https://{host_uri}/{login_endpoint}'.format(
-                    host_uri=HOST_URI,
-                    login_endpoint=LOGIN_ENDPOINT),
-                    json=params,
-                    headers={
-                        'MyQApplicationId': APP_ID
-                    }
+    async def poll_myq(self) -> bool:
+        async with ClientSession() as websession:
+            myq = await pymyq.login(
+                secrets.myq_username, secrets.myq_password, websession
+            )
+            self.doors = myq.devices
+            assert(self.doors is not None)
+            return len(self.doors) > 0
+
+    def update_page(self) -> bool:
+        with file_writer.file_writer(kiosk_constants.myq_pagename) as f:
+            f.write(
+                f"""
+<H1>Garage Door Status</H1>
+<!-- Last updated at {self.last_update} -->
+<HR>
+<TABLE BORDER=0 WIDTH=99%>
+    <TR>
+"""
+            )
+            html = self.do_door("Near House")
+            if html is None:
+                return False
+            f.write(html)
+
+            html = self.do_door("Middle Door")
+            if html is None:
+                return False
+            f.write(html)
+            f.write(
+                """
+    </TR>
+</TABLE>"""
             )
-        auth = login.json()
-        self.myq_security_token = auth.get('SecurityToken')
-        return True
-
-    def get_raw_device_list(self):
-        devices = requests.get(
-            'https://{host_uri}/{device_list_endpoint}'.format(
-                host_uri=HOST_URI,
-                device_list_endpoint=DEVICE_LIST_ENDPOINT),
-                headers={
-                    'MyQApplicationId': APP_ID,
-                    'SecurityToken': self.myq_security_token
-                })
-        return devices.json().get('Devices')
-
-    def get_devices(self):
-        return self.myq_device_list
-
-    def update_devices(self):
-        devices = self.get_raw_device_list()
-        if devices is None:
-            return False
-
-        for dev in devices:
-            if dev["MyQDeviceTypeId"] != 7: continue
-            identifier = str(dev["MyQDeviceId"])
-            update_ts = None
-            state = None
-            name = None
-            for attr in dev["Attributes"]:
-                key = attr["AttributeDisplayName"]
-                value = attr["Value"]
-                if (key == "doorstate"):
-                    state = value
-                    ts = int(attr["UpdatedTime"]) / 1000.0
-                    update_ts = datetime.datetime.fromtimestamp(ts)
-                elif (key == "desc"):
-                    name = value
-            if (identifier in self.myq_device_list):
-                self.myq_device_list[identifier].update(
-                    update_ts, state)
-            else:
-                device = MyQDoor(identifier,
-                                 None,
-                                 name,
-                                 update_ts,
-                                 state,
-                                 self.myq_security_token)
-                self.myq_device_list[identifier] = device
         return True
 
-class garage_door_renderer(renderer.debuggable_abstaining_renderer):
-    def __init__(self, name_to_timeout_dict):
-        super(garage_door_renderer, self).__init__(name_to_timeout_dict, False)
-        self.myq_service = MyQService(secrets.myq_username,
-                                      secrets.myq_password)
-        self.myq_service.login()
-        self.myq_service.update_devices()
-
-    def debug_prefix(self):
-        return "myq"
-
-    def periodic_render(self, key):
-        self.debug_print("*** Executing action %s ***" % key)
-        if key == "Poll MyQ":
-            return self.myq_service.update_devices()
-        elif key == "Update Page":
-            return self.update_page()
+    def get_state_icon(self, state: str) -> str:
+        if state == "open":
+            return "/kiosk/images/garage_open.png"
+        elif state == "closed":
+            return "/kiosk/images/garage_closed.png"
+        elif state == "opening":
+            return "/kiosk/images/garage_opening.png"
+        elif state == "closing":
+            return "/kiosk/images/garage_closing.png"
         else:
-            raise error("Unknown operaiton")
+            return str(state) + ", an unknown state for the door."
 
-    def do_door(self, name):
-        doors = self.myq_service.get_devices()
-        now = datetime.datetime.now()
-        is_night = now.hour <= 7 or now.hour >= 21
-        for key in doors:
-            door = doors[key]
-            if door.get_name() == name:
-                ts = door.get_update_ts()
-                delta = now - ts
-                d = int(delta.total_seconds())
-                days = divmod(d, constants.seconds_per_day)
-                hours = divmod(days[1], constants.seconds_per_hour)
-                minutes = divmod(hours[1], constants.seconds_per_minute)
+    def do_door(self, name: str) -> Optional[str]:
+        if self.doors is None:
+            return None
+        for key in self.doors:
+            door = self.doors[key]
+            if door.name == name:
+                j = self.doors[key].device_json
+                state = j["state"]["door_state"]
+
+                # "last_update": "2020-07-04T18:11:34.2981419Z"
+                raw = j["state"]["last_update"]
+                ts = parse(raw)
+                tz_info = ts.tzinfo
+                now = datetime.datetime.now(tz_info)
+                delta = (now - ts).total_seconds()
+                now = datetime.datetime.now()
+                is_night = now.hour <= 7 or now.hour >= 21
+                duration = datetime_utils.describe_duration_briefly(int(delta))
                 width = 0
-                if is_night and door.get_status() == "open":
+                if is_night and door.state == "open":
                     color = "border-color: #ff0000;"
                     width = 15
                 else:
                     color = ""
                     width = 0
-                return """
-<TD WIDTH=49%%>
+                return f"""
+<TD WIDTH=49%>
   <CENTER>
-  <FONT STYLE="font-size:26pt">%s<BR>
-  <IMG SRC="%s"
+  <FONT STYLE="font-size:26pt">{name}<BR>
+  <IMG SRC="{self.get_state_icon(state)}"
        HEIGHT=250
-       STYLE="border-style: solid; border-width: %dpx; %s">
+       STYLE="border-style: solid; border-width: {width}px; {color}">
   <BR>
-  <B>%s</B></FONT><BR>
-  for %d day(s), %02d:%02d.
+  <B>{state}</B></FONT><BR>
+  for {duration}
   </CENTER>
-</TD>""" % (name,
-            door.get_state_icon(),
-            width,
-            color,
-            door.get_status(),
-            days[0], hours[0], minutes[0])
+</TD>"""
         return None
 
-    def update_page(self):
-        f = file_writer.file_writer(constants.myq_pagename)
-        now = datetime.datetime.now()
-        f.write("""
-<H1>Garage Door Status</H1>
-<!-- Last updated at %s -->
-<HR>
-<TABLE BORDER=0 WIDTH=99%%>
-  <TR>
-""" % now)
-        html = self.do_door("Near House")
-        if html == None:
-            return False
-        f.write(html)
 
-        html = self.do_door("Middle Door")
-        if html == None:
-            return False
-        f.write(html)
-        f.write("""
-  </TR>
-</TABLE>""")
-        f.close()
-        return True
+# Test
+#x = garage_door_renderer({"Test": 1})
+#x.periodic_render("Poll MyQ")
+#x.periodic_render("Update Page")