Skip to content

Commit e3a42ec

Browse files
authored
Merge pull request #2 from home-assistant-ecosystem/retoo-master
Support for shades and automatic discovery 2
2 parents 56a07fe + 386333a commit e3a42ec

File tree

6 files changed

+343
-5
lines changed

6 files changed

+343
-5
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
0.3.0 (--)
5+
----------
6+
7+
- Support for Shades/Blinds (@retoo)
8+
- Access to more parts of the Dingz API
9+
(Blind Config, System Config and others) (@retoo)
10+
- Automatic discovery for Dingz units in the local network (@retoo)
11+
412
0.2.0 (2020-05-20)
513
------------------
614

dingz/constants.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,32 @@
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

4044
# Communication constants
4145
CONTENT_TYPE_JSON = "application/json"
42-
CONTENT_TYPE = 'Content-Type'
46+
CONTENT_TYPE = "Content-Type"
4347
CONTENT_TYPE_TEXT_PLAIN = "text/plain"
48+
49+
DEVICE_MAPPING = {
50+
"102": "myStrom Bulb",
51+
"108": "dingz",
52+
}

dingz/dingz.py

Lines changed: 197 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,24 @@ 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+
"""Get the state."""
140+
url = URL(self.uri).join(URL(STATE))
141+
response = await make_call(self, uri=url)
142+
self._state = response
143+
144+
async def get_blind_config(self) -> None:
145+
"""Get the configuration of a dingz blind part."""
146+
url = URL(self.uri).join(URL(BLIND_CONFIGURATION))
147+
response = await make_call(self, uri=url)
148+
self._blind_config = response
149+
150+
async def get_system_config(self) -> None:
151+
"""Get the system configuration of a dingz."""
152+
url = URL(self.uri).join(URL(SYSTEM_CONFIG))
153+
response = await make_call(self, uri=url)
154+
self._system_config = response
155+
121156
async def enabled(self) -> bool:
122157
"""Return true if front LED is on."""
123158
url = URL(self.uri).join(URL(FRONT_LED_GET))
@@ -136,12 +171,83 @@ async def turn_off(self) -> None:
136171
url = URL(self.uri).join(URL(FRONT_LED_SET))
137172
await make_call(self, uri=url, method="POST", data=data)
138173

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

241+
async def stop_timer(self, data) -> None:
242+
"""Stop a timer."""
243+
url = URL(self.uri).join(URL(TIMER))
244+
await make_call(self, uri=url, method="POST", data=data)
245+
246+
@property
247+
def dingz_name(self) -> str:
248+
"""Get the name of a dingz."""
249+
return self._system_config["dingz_name"]
250+
145251
@property
146252
def device_details(self) -> str:
147253
"""Return the current device details."""
@@ -173,7 +279,7 @@ def wifi_networks(self) -> str:
173279
return self._wifi_networks
174280

175281
@property
176-
def everything(self) -> str:
282+
def everything(self) -> dict:
177283
"""Return the all available device details."""
178284
return self._catch_all
179285

@@ -207,6 +313,95 @@ def intensity(self) -> float:
207313
"""Return the current light intensity in lux."""
208314
return round(self._intensity, 1)
209315

316+
@property
317+
def version(self) -> str:
318+
"""Return the version of a dingz."""
319+
return self._info["version"]
320+
321+
@property
322+
def type(self) -> str:
323+
"""Return the type of a dingz."""
324+
return self._info["type"]
325+
326+
@property
327+
def mac(self) -> str:
328+
"""Return the MAC address of a dingz."""
329+
return self._info["mac"]
330+
331+
@property
332+
def front_hw_model(self) -> str:
333+
"""Get the hardware model of a dingz."""
334+
return self._device_details["front_hw_model"]
335+
336+
@property
337+
def puck_hw_model(self) -> str:
338+
"""Get the hardware model of a dingz puck."""
339+
return self._device_details["puck_hw_model"]
340+
341+
@property
342+
def front_serial_number(self) -> str:
343+
"""Get the serial_number of the a dingz front."""
344+
return self._device_details["front_sn"]
345+
346+
@property
347+
def puck_serial_number(self) -> str:
348+
"""Get the serial number of a dingz puck."""
349+
return self._device_details["puck_sn"]
350+
351+
@property
352+
def hw_version(self) -> str:
353+
"""Get the hardware version of a dingz."""
354+
return self._device_details["hw_version"]
355+
356+
@property
357+
def fw_version(self) -> str:
358+
"""Get the firmware version of a dingz."""
359+
return self._device_details["fw_version"]
360+
361+
def _shade_current_state(self, shade_no: int):
362+
"""Get the configuration of the shade."""
363+
return self._state["blinds"][shade_no]
364+
365+
def current_blind_level(self, shade_no):
366+
"""Get the current blind level."""
367+
return self._shade_current_state(shade_no)["position"]
368+
369+
def current_lamella_level(self, shade_no):
370+
"""Get the current lamella level."""
371+
return self._shade_current_state(shade_no)["lamella"]
372+
373+
def is_shade_closed(self, shade_no):
374+
"""Get the closed state of a shade."""
375+
# When closed, we care if the lamellas are opened or not
376+
return (
377+
self.current_blind_level(shade_no) == 0
378+
and self.current_lamella_level(shade_no) == 0
379+
)
380+
381+
def is_shade_opened(self, shade_no):
382+
"""Get the open state of a shade."""
383+
return self.current_blind_level(shade_no) == 100
384+
385+
def is_lamella_closed(self, shade_no):
386+
"""Get the closed state of a lamella."""
387+
return self.current_lamella_level(shade_no) == 0
388+
389+
def is_lamella_opened(self, shade_no):
390+
"""Get the open state of lamella."""
391+
return self.current_lamella_level(shade_no) == 100
392+
393+
def is_blind_opening(self, shade_no):
394+
"""Get the state of the blind if opening."""
395+
return self._shade_current_state(shade_no)["moving"] == "up"
396+
397+
def is_blind_closing(self, shade_no):
398+
"""Get the state of the blind if closing."""
399+
return self._shade_current_state(shade_no)["moving"] == "down"
400+
401+
def blind_name(self, shade_no):
402+
"""Get the name of the blind."""
403+
return self._blind_config["blinds"][shade_no]["name"]
404+
210405
# See "Using Asyncio in Python" by Caleb Hattingh for implementation
211406
# details.
212407
async def close(self) -> None:

0 commit comments

Comments
 (0)