More python3 changes.
[kiosk.git] / myq_renderer.py
1 #!/usr/local/bin/python
2
3 import requests
4 import os
5 import json
6 import time
7 import constants
8 import datetime
9 import file_writer
10 import globals
11 import http.client
12 import json
13 import renderer
14 import secrets
15
16 APP_ID = "Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB/i"
17 HOST_URI = "myqexternal.myqdevice.com"
18 LOGIN_ENDPOINT = "api/v4/User/Validate"
19 DEVICE_LIST_ENDPOINT = "api/v4/UserDeviceDetails/Get"
20 DEVICE_SET_ENDPOINT = "api/v4/DeviceAttribute/PutDeviceAttribute"
21
22 class MyQDoor:
23     myq_device_id = None
24     myq_lamp_device_id = None
25     myq_device_descr = None
26     myq_device_update_ts = None
27     myq_device_state = None
28     myq_security_token = None
29
30     def __init__(self, device_id, lamp_id, descr, update_ts, state, token):
31         self.myq_device_id = device_id
32         self.myq_lamp_device_id = lamp_id
33         self.myq_device_descr = descr
34         self.myq_device_update_ts = update_ts
35         self.myq_device_state = state
36         self.myq_security_token = token
37
38     def update(self, update_ts, state):
39         self.myq_device_update_ts = update_ts
40         self.myq_device_state = state
41
42     def get_name(self):
43         return self.myq_device_descr
44
45     def get_update_ts(self):
46         return self.myq_device_update_ts
47
48     def open(self):
49         self.change_door_state("open")
50
51     def close(self):
52         self.change_door_state("close")
53
54     def lamp_on(self):
55         self.change_lamp_state("on")
56
57     def lamp_off(self):
58         self.change_lamp_state("off")
59
60     def get_status(self):
61         state = self.myq_device_state
62         if state == "1":
63             return "open"
64         elif state == "2":
65             return "closed"
66         elif state == "3":
67             return "stopped"
68         elif state == "4":
69             return "opening"
70         elif state == "5":
71             return "closing"
72         elif state == "8":
73             return "moving"
74         elif state == "9":
75             return "open"
76         else:
77             return str(state) + ", an unknown state for the door."
78
79     def get_state_icon(self):
80         state = self.myq_device_state
81         if (state == "1" or state == "9"):
82             return "/icons/garage_open.png"
83         elif (state == "2"):
84             return "/icons/garage_closed.png"
85         elif (state == "4"):
86             return "/icons/garage_opening.png"
87         elif (state == "5" or state == "8"):
88             return "/icons/garage_closing.png"
89         else:
90             return str(state) + ", an unknown state for the door."
91
92     def get_lamp_status(self):
93         state = self.check_lamp_state()
94         if state == "0":
95             return "off"
96         elif state == "1":
97             return "on"
98         else:
99             return "unknown"
100
101     def change_device_state(self, payload):
102         device_action = requests.put(
103             'https://{host_uri}/{device_set_endpoint}'.format(
104                 host_uri=HOST_URI,
105                 device_set_endpoint=DEVICE_SET_ENDPOINT),
106                 data=payload,
107                 headers={
108                     'MyQApplicationId': APP_ID,
109                     'SecurityToken': self.myq_security_token
110                 }
111         )
112         return device_action.status_code == 200
113
114     def change_lamp_state(self, command):
115         newstate = 1 if command.lower() == "on" else 0
116         payload = {
117             "attributeName": "desiredlightstate",
118             "myQDeviceId": self.myq_lamp_device_id,
119             "AttributeValue": newstate
120         }
121         return self.change_device_state(payload)
122
123     def change_door_state(self, command):
124         open_close_state = 1 if command.lower() == "open" else 0
125         payload = {
126             'attributeName': 'desireddoorstate',
127             'myQDeviceId': self.myq_device_id,
128             'AttributeValue': open_close_state,
129         }
130         return self.change_device_state(payload)
131
132     def check_door_state(self):
133         return self.myq_device_state
134
135 #    def check_lamp_state(self):
136 #        return self.check_device_state(self.myq_lamp_device_id, "lightstate")
137
138     def __repr__(self):
139         return "MyQ device(%s/%s), last update %s, current state %s" % (
140             self.myq_device_descr,
141             self.myq_device_id,
142             self.myq_device_update_ts,
143             self.get_status());
144
145 class MyQService:
146     myq_security_token = ""
147     myq_device_list = {}
148
149     def __init__(self, username, password):
150         self.username = username
151         self.password = password
152
153     def login(self):
154         params = {
155             'username': self.username,
156             'password': self.password
157         }
158         login = requests.post(
159                 'https://{host_uri}/{login_endpoint}'.format(
160                     host_uri=HOST_URI,
161                     login_endpoint=LOGIN_ENDPOINT),
162                     json=params,
163                     headers={
164                         'MyQApplicationId': APP_ID
165                     }
166             )
167         auth = login.json()
168         self.myq_security_token = auth.get('SecurityToken')
169         return True
170
171     def get_raw_device_list(self):
172         devices = requests.get(
173             'https://{host_uri}/{device_list_endpoint}'.format(
174                 host_uri=HOST_URI,
175                 device_list_endpoint=DEVICE_LIST_ENDPOINT),
176                 headers={
177                     'MyQApplicationId': APP_ID,
178                     'SecurityToken': self.myq_security_token
179                 })
180         return devices.json().get('Devices')
181
182     def get_devices(self):
183         return self.myq_device_list
184
185     def update_devices(self):
186         devices = self.get_raw_device_list()
187         if devices is None:
188             return False
189
190         for dev in devices:
191             if dev["MyQDeviceTypeId"] != 7: continue
192             identifier = str(dev["MyQDeviceId"])
193             update_ts = None
194             state = None
195             name = None
196             for attr in dev["Attributes"]:
197                 key = attr["AttributeDisplayName"]
198                 value = attr["Value"]
199                 if (key == "doorstate"):
200                     state = value
201                     ts = int(attr["UpdatedTime"]) / 1000.0
202                     update_ts = datetime.datetime.fromtimestamp(ts)
203                 elif (key == "desc"):
204                     name = value
205             if (identifier in self.myq_device_list):
206                 self.myq_device_list[identifier].update(
207                     update_ts, state)
208             else:
209                 device = MyQDoor(identifier,
210                                  None,
211                                  name,
212                                  update_ts,
213                                  state,
214                                  self.myq_security_token)
215                 self.myq_device_list[identifier] = device
216         return True
217
218 class garage_door_renderer(renderer.debuggable_abstaining_renderer):
219     def __init__(self, name_to_timeout_dict):
220         super(garage_door_renderer, self).__init__(name_to_timeout_dict, False)
221         self.myq_service = MyQService(secrets.myq_username,
222                                       secrets.myq_password)
223         self.myq_service.login()
224         self.myq_service.update_devices()
225
226     def debug_prefix(self):
227         return "myq"
228
229     def periodic_render(self, key):
230         self.debug_print("*** Executing action %s ***" % key)
231         if key == "Poll MyQ":
232             return self.myq_service.update_devices()
233         elif key == "Update Page":
234             return self.update_page()
235         else:
236             raise error("Unknown operaiton")
237
238     def do_door(self, name):
239         doors = self.myq_service.get_devices()
240         now = datetime.datetime.now()
241         is_night = now.hour <= 7 or now.hour >= 21
242         for key in doors:
243             door = doors[key]
244             if door.get_name() == name:
245                 ts = door.get_update_ts()
246                 delta = now - ts
247                 d = int(delta.total_seconds())
248                 days = divmod(d, constants.seconds_per_day)
249                 hours = divmod(days[1], constants.seconds_per_hour)
250                 minutes = divmod(hours[1], constants.seconds_per_minute)
251                 width = 0
252                 if is_night and door.get_status() == "open":
253                     color = "border-color: #ff0000;"
254                     width = 15
255                 else:
256                     color = ""
257                     width = 0
258                 return """
259 <TD WIDTH=49%%>
260   <CENTER>
261   <FONT STYLE="font-size:26pt">%s<BR>
262   <IMG SRC="%s"
263        HEIGHT=250
264        STYLE="border-style: solid; border-width: %dpx; %s">
265   <BR>
266   <B>%s</B></FONT><BR>
267   for %d day(s), %02d:%02d.
268   </CENTER>
269 </TD>""" % (name,
270             door.get_state_icon(),
271             width,
272             color,
273             door.get_status(),
274             days[0], hours[0], minutes[0])
275         return None
276
277     def update_page(self):
278         f = file_writer.file_writer(constants.myq_pagename)
279         now = datetime.datetime.now()
280         f.write("""
281 <H1>Garage Door Status</H1>
282 <!-- Last updated at %s -->
283 <HR>
284 <TABLE BORDER=0 WIDTH=99%%>
285   <TR>
286 """ % now)
287         html = self.do_door("Near House")
288         if html == None:
289             return False
290         f.write(html)
291
292         html = self.do_door("Middle Door")
293         if html == None:
294             return False
295         f.write(html)
296         f.write("""
297   </TR>
298 </TABLE>""")
299         f.close()
300         return True