Merge branch 'master' of ssh://wwwjail.house/usr/local/git/base/kiosk
[kiosk.git] / myq_renderer.py
1 #!/usr/bin/env python3
2
3 from aiohttp import ClientSession
4 import asyncio
5 import datetime
6 from dateutil.parser import parse
7 import pymyq  # type: ignore
8 from typing import Dict, Optional
9
10 from pyutils.datetimes import datetime_utils
11
12 import kiosk_constants
13 import file_writer
14 import renderer
15 import kiosk_secrets as secrets
16
17
18 class garage_door_renderer(renderer.abstaining_renderer):
19     def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None:
20         super().__init__(name_to_timeout_dict)
21         self.doors: Optional[Dict] = None
22         self.last_update: Optional[datetime.datetime] = None
23
24     def debug_prefix(self) -> str:
25         return "myq"
26
27     def periodic_render(self, key: str) -> bool:
28         if key == "Poll MyQ":
29             self.last_update = datetime.datetime.now()
30             return asyncio.run(self.poll_myq())
31         elif key == "Update Page":
32             return self.update_page()
33         else:
34             raise Exception("Unknown operaiton")
35
36     async def poll_myq(self) -> bool:
37         async with ClientSession() as websession:
38             myq = await pymyq.login(
39                 secrets.myq_username, secrets.myq_password, websession
40             )
41             self.doors = myq.devices
42             assert(self.doors is not None)
43             return len(self.doors) > 0
44
45     def update_page(self) -> bool:
46         with file_writer.file_writer(kiosk_constants.myq_pagename) as f:
47             f.write(
48                 f"""
49 <H1>Garage Door Status</H1>
50 <!-- Last updated at {self.last_update} -->
51 <HR>
52 <TABLE BORDER=0 WIDTH=99%>
53     <TR>
54 """
55             )
56             html = self.do_door("Near House")
57             if html is None:
58                 return False
59             f.write(html)
60
61             html = self.do_door("Middle Door")
62             if html is None:
63                 return False
64             f.write(html)
65             f.write(
66                 """
67     </TR>
68 </TABLE>"""
69             )
70         return True
71
72     def get_state_icon(self, state: str) -> str:
73         if state == "open":
74             return "/kiosk/images/garage_open.png"
75         elif state == "closed":
76             return "/kiosk/images/garage_closed.png"
77         elif state == "opening":
78             return "/kiosk/images/garage_opening.png"
79         elif state == "closing":
80             return "/kiosk/images/garage_closing.png"
81         else:
82             return str(state) + ", an unknown state for the door."
83
84     def do_door(self, name: str) -> Optional[str]:
85         if self.doors is None:
86             return None
87         for key in self.doors:
88             door = self.doors[key]
89             if door.name == name:
90                 j = self.doors[key].device_json
91                 state = j["state"]["door_state"]
92
93                 # "last_update": "2020-07-04T18:11:34.2981419Z"
94                 raw = j["state"]["last_update"]
95                 ts = parse(raw)
96                 tz_info = ts.tzinfo
97                 now = datetime.datetime.now(tz_info)
98                 delta = (now - ts).total_seconds()
99                 now = datetime.datetime.now()
100                 is_night = now.hour <= 7 or now.hour >= 21
101                 duration = datetime_utils.describe_duration_briefly(int(delta))
102                 width = 0
103                 if is_night and door.state == "open":
104                     color = "border-color: #ff0000;"
105                     width = 15
106                 else:
107                     color = ""
108                     width = 0
109                 return f"""
110 <TD WIDTH=49%>
111   <CENTER>
112   <FONT STYLE="font-size:26pt">{name}<BR>
113   <IMG SRC="{self.get_state_icon(state)}"
114        HEIGHT=250
115        STYLE="border-style: solid; border-width: {width}px; {color}">
116   <BR>
117   <B>{state}</B></FONT><BR>
118   for {duration}
119   </CENTER>
120 </TD>"""
121         return None
122
123
124 # Test
125 #x = garage_door_renderer({"Test": 1})
126 #x.periodic_render("Poll MyQ")
127 #x.periodic_render("Update Page")