Skip to content

Commit dbb21e3

Browse files
committed
Support for shades and other API endpoints
1 parent 56a07fe commit dbb21e3

File tree

3 files changed

+245
-2
lines changed

3 files changed

+245
-2
lines changed

dingz/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,23 @@
2121
LIGHT = "light"
2222
SCHEDULE = "schedule"
2323
TIMER = "timer"
24+
SHADE = "shade"
25+
STATE = "state"
2426

2527
# Special endpoints
2628
LOG = "/log"
2729
FIRMWARE = "/load"
2830

2931
SETTINGS = "settings"
32+
SYSTEM_CONFIG = "system_config"
3033
FRONT_LED_GET = "led/get"
3134
FRONT_LED_SET = "led/set"
3235
BUTTON_ACTIONS = "action"
3336
WIFI_SCAN = "scan"
3437

3538
# Configuration endpoints
3639
PIR_CONFIGURATION = "pir_config"
40+
BLIND_CONFIGURATION = "blind_config"
3741
THERMOSTAT_CONFIGURATION = "thermostat_config"
3842
INPUT_CONFIGURATION = "input_config"
3943

dingz/dingz.py

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
WIFI_SCAN,
2222
TIMER,
2323
SCHEDULE,
24+
SHADE,
25+
INFO,
26+
STATE,
27+
SYSTEM_CONFIG,
28+
BLIND_CONFIGURATION,
2429
)
2530

2631
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +40,7 @@ def __init__(self, host: str, session: aiohttp.client.ClientSession = None) -> N
3540
self._host = host
3641
self._session = session
3742
self._device_details = None
43+
self._info = None
3844
self._wifi_networks = None
3945
self._settings = None
4046
self._catch_all = {}
@@ -47,15 +53,26 @@ def __init__(self, host: str, session: aiohttp.client.ClientSession = None) -> N
4753
self._motion = None
4854
self._schedule = None
4955
self._timer = None
56+
self._state = {}
57+
self._blind_config = None
58+
self._system_config = None
59+
5060
self.uri = URL.build(scheme="http", host=self._host).join(URL(API))
5161

5262
async def get_device_info(self) -> None:
5363
"""Get the details from the dingz."""
5464
url = URL(self.uri).join(URL(DEVICE_INFO))
5565
response = await make_call(self, uri=url)
56-
self._device_details = response
66+
# response is: "mac" => { device_details }
67+
self._device_details = next(iter(response.values()))
5768

5869
async def get_info(self) -> None:
70+
"""Get general information fro the dingz."""
71+
url = URL(self.uri).join(URL(INFO))
72+
response = await make_call(self, uri=url)
73+
self._info = response
74+
75+
async def get_all_info(self) -> None:
5976
"""Get everything from the dingz unit."""
6077
for endpoint in [
6178
PUCK,
@@ -118,6 +135,21 @@ async def get_light(self) -> None:
118135
self._intensity = response["intensity"]
119136
self._hour_of_day = response["state"]
120137

138+
async def get_state(self) -> None:
139+
url = URL(self.uri).join(URL(STATE))
140+
response = await make_call(self, uri=url)
141+
self._state = response
142+
143+
async def get_blind_config(self) -> None:
144+
url = URL(self.uri).join(URL(BLIND_CONFIGURATION))
145+
response = await make_call(self, uri=url)
146+
self._blind_config = response
147+
148+
async def get_system_config(self) -> None:
149+
url = URL(self.uri).join(URL(SYSTEM_CONFIG))
150+
response = await make_call(self, uri=url)
151+
self._system_config = response
152+
121153
async def enabled(self) -> bool:
122154
"""Return true if front LED is on."""
123155
url = URL(self.uri).join(URL(FRONT_LED_GET))
@@ -136,12 +168,69 @@ async def turn_off(self) -> None:
136168
url = URL(self.uri).join(URL(FRONT_LED_SET))
137169
await make_call(self, uri=url, method="POST", data=data)
138170

171+
async def operate_shade(self, shade_no, blind=None, lamella=None) -> None:
172+
"""operate the lamella and blind
173+
blind: 0 fully closed, 100 fully open
174+
lamella: 0 lamellas closed, 100 lamellas open
175+
"""
176+
177+
# With newer versions of dingz, we can just leave
178+
# either lamella or blind None (i.e. do not chang)
179+
# but currently we need to lookup the current state
180+
# of the shade first.
181+
if blind is None or lamella is None:
182+
await self.get_state()
183+
184+
if blind is None:
185+
blind = self.current_blind_level(shade_no)
186+
187+
if lamella is None:
188+
if self.is_shade_opened(shade_no):
189+
# if the shade is currently completely opened (i.e. up), the lamella
190+
# value is not really relevant (has no effect). We assume the
191+
# lamella value to be 0, ie. closed.
192+
# i.e. we set lamella to 45, raise blind to the top, and then back down again
193+
# => de we expect the lamella to be set to 45 again, or does it get resetted to 0?
194+
lamella = 0
195+
else:
196+
lamella = self.current_lamella_level(shade_no)
197+
198+
url = URL(self.uri).join(URL("%s/%s" % (SHADE, shade_no)))
199+
params = {"blind": str(blind), "lamella": str(lamella)}
200+
await make_call(self, uri=url, method="POST", parameters=params)
201+
202+
async def shade_up(self, shade_no) -> None:
203+
await self.shade_command(shade_no, "up")
204+
205+
async def shade_down(self, shade_no) -> None:
206+
await self.shade_command(shade_no, "down")
207+
208+
async def shade_stop(self, shade_no) -> None:
209+
await self.shade_command(shade_no, "stop")
210+
211+
async def lamella_open(self, shade_no) -> None:
212+
await self.operate_shade(shade_no, lamella=100)
213+
214+
async def lamella_close(self, shade_no) -> None:
215+
await self.operate_shade(shade_no, lamella=0)
216+
217+
async def lamella_stop(self, shade_no) -> None:
218+
await self.shade_stop(shade_no)
219+
220+
async def shade_command(self, shade_no, verb):
221+
url = URL(self.uri).join(URL("%s/%s/%s" % (SHADE, shade_no, verb)))
222+
await make_call(self, uri=url, method="POST")
223+
139224
async def set_timer(self, data) -> None:
140225
"""Set a timer."""
141226
print(data)
142227
url = URL(self.uri).join(URL(TIMER))
143228
await make_call(self, uri=url, method="POST", json_data=data)
144229

230+
@property
231+
def dingz_name(self) -> str:
232+
return self._system_config["dingz_name"]
233+
145234
@property
146235
def device_details(self) -> str:
147236
"""Return the current device details."""
@@ -173,7 +262,7 @@ def wifi_networks(self) -> str:
173262
return self._wifi_networks
174263

175264
@property
176-
def everything(self) -> str:
265+
def everything(self) -> dict:
177266
"""Return the all available device details."""
178267
return self._catch_all
179268

@@ -207,6 +296,76 @@ def intensity(self) -> float:
207296
"""Return the current light intensity in lux."""
208297
return round(self._intensity, 1)
209298

299+
@property
300+
def version(self) -> str:
301+
return self._info["version"]
302+
303+
@property
304+
def type(self) -> str:
305+
return self._info["type"]
306+
307+
@property
308+
def mac(self) -> str:
309+
return self._info["mac"]
310+
311+
@property
312+
def front_hw_model(self) -> str:
313+
return self._device_details["front_hw_model"]
314+
315+
@property
316+
def puck_hw_model(self) -> str:
317+
return self._device_details["puck_hw_model"]
318+
319+
@property
320+
def front_sn(self) -> str:
321+
return self._device_details["front_sn"]
322+
323+
@property
324+
def puck_sn(self) -> str:
325+
return self._device_details["puck_sn"]
326+
327+
@property
328+
def hw_version(self) -> str:
329+
return self._device_details["hw_version"]
330+
331+
@property
332+
def fw_version(self) -> str:
333+
return self._device_details["fw_version"]
334+
335+
def _shade_current_state(self, shade_no: int):
336+
return self._state["blinds"][shade_no]
337+
338+
def current_blind_level(self, shade_no):
339+
return self._shade_current_state(shade_no)["position"]
340+
341+
def current_lamella_level(self, shade_no):
342+
return self._shade_current_state(shade_no)["lamella"]
343+
344+
def is_shade_closed(self, shade_no):
345+
# when closed, we care if the lamellas are opened or not
346+
return (
347+
self.current_blind_level(shade_no) == 0
348+
and self.current_lamella_level(shade_no) == 0
349+
)
350+
351+
def is_shade_opened(self, shade_no):
352+
return self.current_blind_level(shade_no) == 100
353+
354+
def is_lamella_closed(self, shade_no):
355+
return self.current_lamella_level(shade_no) == 0
356+
357+
def is_lamella_opened(self, shade_no):
358+
return self.current_lamella_level(shade_no) == 100
359+
360+
def is_blind_opening(self, shade_no):
361+
return self._shade_current_state(shade_no)["moving"] == "up"
362+
363+
def is_blind_closing(self, shade_no):
364+
return self._shade_current_state(shade_no)["moving"] == "down"
365+
366+
def blind_name(self, shade_no):
367+
return self._blind_config["blinds"][shade_no]["name"]
368+
210369
# See "Using Asyncio in Python" by Caleb Hattingh for implementation
211370
# details.
212371
async def close(self) -> None:

dingz/discovery.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
import asyncio
3+
import logging
4+
from typing import Optional, List
5+
6+
_LOGGER = logging.getLogger(__name__)
7+
8+
class DiscoveredDevice(object):
9+
mac: str
10+
type: int
11+
is_child: bool
12+
mystrom_registered: bool
13+
mystrom_online: bool
14+
restarted: bool
15+
16+
@staticmethod
17+
def create_from_announce_msg(raw_addr, announce_msg):
18+
if len(announce_msg) != 8:
19+
raise RuntimeError("unexpected announcement, %s" % announce_msg)
20+
21+
device = DiscoveredDevice(host=raw_addr[0],
22+
mac=announce_msg[0:6].hex(":"))
23+
device.type = announce_msg[6]
24+
status = announce_msg[7]
25+
device.is_child = status & 1 != 0
26+
device.mystrom_registered = status & 2 != 0
27+
device.mystrom_online = status & 4 != 0
28+
device.restarted = status & 8 != 0
29+
return device
30+
31+
def __init__(self, host, mac):
32+
self.host = host
33+
self.mac = mac
34+
35+
36+
class DiscoveryProtocol(asyncio.DatagramProtocol):
37+
def __init__(self, registry):
38+
super().__init__()
39+
self.registry = registry
40+
41+
def connection_made(self, transport):
42+
self.transport = transport
43+
44+
def datagram_received(self, data, addr):
45+
device = DiscoveredDevice.create_from_announce_msg(addr, data)
46+
self.registry.register(device)
47+
48+
def connection_lost(self, exc: Optional[Exception]) -> None:
49+
super().connection_lost(exc)
50+
51+
52+
class DeviceRegistry(object):
53+
def __init__(self):
54+
self.devices_by_mac = {}
55+
56+
def register(self, device):
57+
self.devices_by_mac[device.mac] = device
58+
59+
def devices(self):
60+
return list(self.devices_by_mac.values())
61+
62+
63+
async def discover_dingz_devices(timeout=7) -> List[DiscoveredDevice]:
64+
registry = DeviceRegistry()
65+
loop = asyncio.get_event_loop()
66+
(transport, protocol) = await loop.create_datagram_endpoint(lambda: DiscoveryProtocol(registry), local_addr=("0.0.0.0", 7979))
67+
68+
await asyncio.sleep(timeout)
69+
transport.close()
70+
devices = registry.devices()
71+
return devices
72+
73+
74+
if __name__ == "__main__":
75+
loop = asyncio.get_event_loop()
76+
devices = asyncio.run(discover_dingz_devices())
77+
78+
print("found %s devices" % len(devices))
79+
for device in devices:
80+
print(f'{device.mac} {device.host}')

0 commit comments

Comments
 (0)