From c3650cf7210df2a2f3ebb2b0d12b765017b74b43 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Wed, 23 Oct 2019 18:13:47 +0200 Subject: [PATCH] Add support for signal repeaters (#251) * Move constant to a more logical place * Separate blind control into separate class * Experimental support for stopping the blind * Rename method * Add signal repeater class * Add repeater example * Typo * Fix typo * Minor fixes * Lint --- examples/example_cover_async.py | 137 ++++++++++++++++++++ pytradfri/const.py | 2 +- pytradfri/device/__init__.py | 14 +- pytradfri/device/blind_control.py | 18 ++- pytradfri/device/signal_repeater.py | 15 +++ pytradfri/device/signal_repeater_control.py | 26 ++++ setup.py | 2 +- 7 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 examples/example_cover_async.py create mode 100644 pytradfri/device/signal_repeater.py create mode 100644 pytradfri/device/signal_repeater_control.py diff --git a/examples/example_cover_async.py b/examples/example_cover_async.py new file mode 100644 index 00000000..117924c9 --- /dev/null +++ b/examples/example_cover_async.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +This is an example of how the pytradfri-library can be used async. + +To run the script, do the following: +$ pip3 install pytradfri +$ Download this file (example_async.py) +$ python3 example_async.py + +Where is the address to your IKEA gateway. The first time +running you will be asked to input the 'Security Code' found on +the back of your IKEA gateway. +""" + +# Hack to allow relative import above top level package +import sys +import os + +folder = os.path.dirname(os.path.abspath(__file__)) # noqa +sys.path.insert(0, os.path.normpath("%s/.." % folder)) # noqa + +from pytradfri import Gateway +from pytradfri.api.aiocoap_api import APIFactory +from pytradfri.error import PytradfriError +from pytradfri.util import load_json, save_json + +import asyncio +import uuid +import argparse + +CONFIG_FILE = 'tradfri_standalone_psk.conf' + +parser = argparse.ArgumentParser() +parser.add_argument('host', metavar='IP', type=str, + help='IP Address of your Tradfri gateway') +parser.add_argument('-K', '--key', dest='key', required=False, + help='Key found on your Tradfri gateway') +args = parser.parse_args() + +if args.host not in load_json(CONFIG_FILE) and args.key is None: + print("Please provide the 'Security Code' on the back of your " + "Tradfri gateway:", end=" ") + key = input().strip() + if len(key) != 16: + raise PytradfriError("Invalid 'Security Code' provided.") + else: + args.key = key + + +async def run(): + # Assign configuration variables. + # The configuration check takes care they are present. + conf = load_json(CONFIG_FILE) + + try: + identity = conf[args.host].get('identity') + psk = conf[args.host].get('key') + api_factory = APIFactory(host=args.host, psk_id=identity, psk=psk) + except KeyError: + identity = uuid.uuid4().hex + api_factory = APIFactory(host=args.host, psk_id=identity) + + try: + psk = await api_factory.generate_psk(args.key) + print('Generated PSK: ', psk) + + conf[args.host] = {'identity': identity, + 'key': psk} + save_json(CONFIG_FILE, conf) + except AttributeError: + raise PytradfriError("Please provide the 'Security Code' on the " + "back of your Tradfri gateway using the " + "-K flag.") + + api = api_factory.request + + gateway = Gateway() + + devices_command = gateway.get_devices() + devices_commands = await api(devices_command) + devices = await api(devices_commands) + + blinds = [dev for dev in devices if dev.has_blind_control] + repeaters = [dev for dev in devices if dev.has_signal_repeater_control] + + # Print all sockets + print("All blinds") + print(blinds) + + print("All repeatersK") + print(repeaters) + + # Sockets can be accessed by its index, so sockets[1] is the second blind + if blinds: + blind = blinds[0] + else: + print("No sockets found!") + blind = None + + def observe_callback(updated_device): + blind = updated_device.blind_control.blinds[0] + print("Received message for: %s" % blind) + + def observe_err_callback(err): + print('observe error:', err) + + for blind in blinds: + observe_command = blind.observe(observe_callback, + observe_err_callback, + duration=120) + # Start observation as a second task on the loop. + asyncio.ensure_future(api(observe_command)) + # Yield to allow observing to start. + await asyncio.sleep(0) + + if blind: + # Example 1: What is the name of the blind + print("Name:", blind.name) + + # Example 2: checks current battery level of blind + print("Battery (%):", blind.device_info.battery_level) + + # Current level of the blind + print("Battery (%):", blinds[0].blind_control.blinds[0]. + current_cover_position) + + # Example 3: Set blind to 50% open + state_command = blinds[0].blind_control.set_state(50) + await api(state_command) + + print("Waiting for observation to end (30 secs)") + await asyncio.sleep(30) + + await api_factory.shutdown() + + +asyncio.get_event_loop().run_until_complete(run()) diff --git a/pytradfri/const.py b/pytradfri/const.py index 8d1d8fa5..e2cff669 100644 --- a/pytradfri/const.py +++ b/pytradfri/const.py @@ -7,6 +7,7 @@ ROOT_SIGNAL_REPEATER = "15014" ROOT_SMART_TASKS = "15010" ROOT_START_ACTION = "15013" # found under ATTR_START_ACTION +ATTR_START_BLINDS = "15015" ATTR_ALEXA_PAIR_STATUS = "9093" ATTR_AUTH = "9063" @@ -96,7 +97,6 @@ ATTR_SENSOR_UNIT = "5701" ATTR_SENSOR_VALUE = "5700" ATTR_START_ACTION = "9042" # array -ATTR_START_BLINDS = "15015" ATTR_SMART_TASK_TYPE = "9040" # 4 = transition | 1 = not home | 2 = on/off ATTR_SMART_TASK_NOT_AT_HOME = 1 ATTR_SMART_TASK_LIGHTS_OFF = 2 diff --git a/pytradfri/device/__init__.py b/pytradfri/device/__init__.py index 1a68706f..ede5ae5b 100644 --- a/pytradfri/device/__init__.py +++ b/pytradfri/device/__init__.py @@ -3,9 +3,11 @@ from pytradfri.const import ATTR_APPLICATION_TYPE, ROOT_DEVICES, \ ATTR_LAST_SEEN, ATTR_REACHABLE_STATE, \ - ATTR_LIGHT_CONTROL, ATTR_SWITCH_PLUG, ATTR_DEVICE_INFO, ATTR_START_BLINDS + ATTR_LIGHT_CONTROL, ATTR_SWITCH_PLUG, ATTR_DEVICE_INFO,\ + ATTR_START_BLINDS, ROOT_SIGNAL_REPEATER from pytradfri.device.blind_control import BlindControl from pytradfri.device.light_control import LightControl +from pytradfri.device.signal_repeater_control import SignalRepeaterControl from pytradfri.device.socket_control import SocketControl from pytradfri.resource import ApiResource @@ -64,6 +66,16 @@ def blind_control(self): if self.has_blind_control: return BlindControl(self) + @property + def has_signal_repeater_control(self): + return (self.raw is not None and + len(self.raw.get(ROOT_SIGNAL_REPEATER, "")) > 0) + + @property + def signal_repeater_control(self): + if self.has_signal_repeater_control: + return SignalRepeaterControl(self) + def __repr__(self): return "<{} - {} ({})>".format(self.id, self.name, self.device_info.model_number) diff --git a/pytradfri/device/blind_control.py b/pytradfri/device/blind_control.py index 72590560..aae1628a 100644 --- a/pytradfri/device/blind_control.py +++ b/pytradfri/device/blind_control.py @@ -1,7 +1,7 @@ """Class to control the blinds.""" from pytradfri.command import Command from pytradfri.const import RANGE_BLIND, \ - ATTR_BLIND_CURRENT_POSITION, ATTR_START_BLINDS + ATTR_BLIND_CURRENT_POSITION, ATTR_START_BLINDS, ATTR_BLIND_TRIGGER from pytradfri.device.blind import Blind from pytradfri.device.controller import Controller @@ -22,12 +22,22 @@ def blinds(self): """Return blind objects of the blind control.""" return [Blind(self._device, i) for i in range(len(self.raw))] + def trigger_blind(self): + """Trigger the blind's movement.""" + return self.set_value( + { + ATTR_BLIND_TRIGGER: True + } + ) + def set_state(self, state): """Set state of a blind.""" self._value_validate(state, RANGE_BLIND, "Blind position") return self.set_value( - state + { + ATTR_BLIND_CURRENT_POSITION: state + } ) def set_value(self, value): @@ -38,8 +48,6 @@ def set_value(self, value): return Command('put', self._device.path, { ATTR_START_BLINDS: [ - { - ATTR_BLIND_CURRENT_POSITION: value - } + value ] }) diff --git a/pytradfri/device/signal_repeater.py b/pytradfri/device/signal_repeater.py new file mode 100644 index 00000000..f80748d7 --- /dev/null +++ b/pytradfri/device/signal_repeater.py @@ -0,0 +1,15 @@ +"""Represent a signal repeater.""" +from pytradfri.const import ROOT_SIGNAL_REPEATER + + +class SignalRepeater: + """Represent a signal repeater.""" + + def __init__(self, device, index): + self.device = device + self.index = index + + @property + def raw(self): + """Return raw data that it represents.""" + return self.device.raw[ROOT_SIGNAL_REPEATER][self.index] diff --git a/pytradfri/device/signal_repeater_control.py b/pytradfri/device/signal_repeater_control.py new file mode 100644 index 00000000..1719dbf0 --- /dev/null +++ b/pytradfri/device/signal_repeater_control.py @@ -0,0 +1,26 @@ +"""Class to control the signal repeater.""" +from pytradfri.const import ROOT_SIGNAL_REPEATER +from pytradfri.device.signal_repeater import SignalRepeater + + +class SignalRepeaterControl: + """Class to control the signal repeaters.""" + + def __init__(self, device): + self._device = device + + @property + def raw(self): + """Return raw data that it represents.""" + return self._device.raw[ROOT_SIGNAL_REPEATER] + + @property + def signal_repeaters(self): + """Return signal repeater objects of the signal repeater control.""" + return [SignalRepeater(self._device, i) for i in range(len(self.raw))] + + def __repr__(self): + return ''.format( + self._device.name, + len(self.raw) + ) diff --git a/setup.py b/setup.py index 4ef753d0..0b815fec 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() -VERSION = "6.3.1" +VERSION = "6.4.0" DOWNLOAD_URL = \ 'https://github.com/ggravlingen/pytradfri/archive/{}.zip'.format(VERSION)