-#!/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 http.client
-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")