From 546fcb6d9b27a708c5f0f07cc8a5a56c6ec39cd7 Mon Sep 17 00:00:00 2001 From: Ben Dews Date: Wed, 7 Nov 2018 22:32:13 +1100 Subject: [PATCH] Initial Commit --- .gitignore | 2 + README.md | 62 +++++++++++++++- setup.py | 23 ++++++ somfy_mylink_synergy/__init__.py | 121 +++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 setup.py create mode 100644 somfy_mylink_synergy/__init__.py diff --git a/.gitignore b/.gitignore index 894a44c..5249283 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ venv.bak/ # mypy .mypy_cache/ +.vscode +Pipfile* diff --git a/README.md b/README.md index a6d3dfc..0134bce 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,60 @@ -# somfy-mylink-api -API to simplify interactions with a Somfy MyLink device +[![Build Status](https://travis-ci.org/bendews/somfy-mylink-synergy.svg?branch=master)](https://travis-ci.org/bendews/somfy-mylink-synergy) + +# Somfy MyLink Synergy API + +Python API to utilise the Somfy Synergy API utilising JsonRPC. + +## Requirements + +- Python >= 3.4 + +## Usage +```python + +import asyncio +from somfy_mylink_synergy import SomfyMyLinkSynergy + +loop = asyncio.get_event_loop() +mylink = SomfyMyLinkSynergy('YourSystemID', '10.1.1.50') + + +mylink_status = loop.run_until_complete(mylink.status_info()) +for device in mylink_status['result']: + print(device['targetID'], device['name']) + +> ('CC0000A.1', 'Bedroom Cover') +> ('CC0000A.2', 'Kitchen Cover') + +mylink_status = loop.run_until_complete(mylink.scene_list()) +for scene in mylink_status['result']: + print(device['targetID'], device['name']) + +> ('123456789', 'Morning') +> ('987654321', 'Evening') + +mylink_ping= loop.run_until_complete(mylink.status_ping()) +for device in mylink_status['result']: + print(device) + +> ('CC0000A.1') +> ('CC0000A.2') + +open_cover = loop.run_until_complete(mylink.move_up('CC0000A.1')) +close_cover = loop.run_until_complete(mylink.move_down('CC0000A.1')) +stop_cover = loop.run_until_complete(mylink.move_stop('CC0000A.1')) +activate_scene = loop.run_until_complete(mylink.scene_run('123456789')) + +``` + + +## TODO: + +- None + +## License + +MIT + +## Author Information + +Created in 2018 by [Ben Dews](https://bendews.com) \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c628325 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +""" +Install Somfy MyLink Synergy API +""" + +from setuptools import setup, find_packages + +with open('README.md') as f: + long_description = f.read() + +setup( + name='somfy_mylink_synergy', + version='1.0.1', + url='http://github.com/bendews/somfy-mylink-synergy', + license='MIT', + author='Ben Dews', + author_email='contact@bendews.com', + description='Python API to utilise the Somfy Synergy JsonRPC API', + long_description=long_description, + long_description_content_type='text/markdown', + packages=find_packages(), + keywords='somfy mylink synergy covers sensors api jsonrpc', + platforms='any' +) diff --git a/somfy_mylink_synergy/__init__.py b/somfy_mylink_synergy/__init__.py new file mode 100644 index 0000000..4d4a8e7 --- /dev/null +++ b/somfy_mylink_synergy/__init__.py @@ -0,0 +1,121 @@ +import json +import logging +import asyncio +from random import randint + +_LOGGER = logging.getLogger(__name__) + + +class SomfyMyLinkSynergy: + """API Wrapper for the Somfy MyLink device.""" + + def __init__(self, system_id, host, port=44100, timeout=3): + """Create the object with required parameters.""" + self.host = host + self.port = port + self.system_id = system_id + self._timeout = timeout + self._stream_reader = None + self._stream_writer = None + + async def scene_list(self): + """List all Somfy scenes.""" + return await self.command("mylink.scene.list") + + async def scene_run(self, scene_id): + """Run specified Somfy scene.""" + return await self.command("mylink.scene.run", sceneID=scene_id) + + async def status_info(self, target_id="*.*"): + """Retrieve info on all Somfy devices.""" + return await self.command("mylink.status.info", targetID=target_id) + + async def status_ping(self, target_id="*.*"): + """Send a Ping message to all Somfy devices.""" + return await self.command("mylink.status.ping", targetID=target_id) + + async def move_up(self, target_id="*.*"): + """Format a Move up message and send it.""" + return await self.command("mylink.move.up", targetID=target_id) + + async def move_down(self, target_id="*.*"): + """Format a Move Down message and send it.""" + return await self.command("mylink.move.down", targetID=target_id) + + async def move_stop(self, target_id="*.*"): + """Format a Stop message and send it.""" + return await self.command("mylink.move.stop", targetID=target_id) + + async def command(self, method, **kwargs): + """Format a Somfy JSON API message.""" + params = dict(**kwargs) + params.setdefault('auth', self.system_id) + # Set a random message ID + message_id = randint(0, 1000) + message = dict(method=method, params=params, id=message_id) + return await self.send_message(message) + + async def send_message(self, message): + """Send a Somfy JSON API message and gather response.""" + # Substring to search in response string to signify end of the message + # MyLink always returns message 'id' as last key so we search for that + # print(read_until_string) + # > b'"id":3}' + message_id_bytes = str(message['id']).encode('utf-8') + read_until_string = b'"id":'+message_id_bytes+b'}' + try: + await self._send_data(message) + return await self._recieve_data(read_until_string) + except UnicodeDecodeError as unicode_error: + _LOGGER.info('Message collision, trying again: %s', unicode_error) + return await self.send_message(message) + + async def _make_connection(self): + """Open asyncio socket connection with MyLink device.""" + if self._stream_writer: + _LOGGER.debug('Reusing existing socket connection to %s on %s', + self.host, self.port) + return + _LOGGER.debug('Opening new socket connection to %s on %s', + self.host, self.port) + conn = asyncio.open_connection(self.host, self.port) + conn_wait = asyncio.wait_for(conn, timeout=self._timeout) + try: + self._stream_reader, self._stream_writer = await conn_wait + except (asyncio.TimeoutError, ConnectionRefusedError, OSError) as timeout_err: + _LOGGER.error('Connection failed for %s on %s. ' + 'Please ensure device is reachable.', + self.host, self.port) + raise timeout_err + + async def _send_data(self, data): + """Send data to MyLink using JsonRPC via Socket.""" + await self._make_connection() + try: + data_as_bytes = str.encode(json.dumps(data)) + self._stream_writer.write(data_as_bytes) + except TypeError as data_error: + _LOGGER.error('Invalid data sent to device') + raise data_error + + async def _recieve_data(self, read_until=None): + """Recieve Data from MyLink using JsonRPC via Socket.""" + await self._make_connection() + try: + if read_until: + reader = self._stream_reader.readuntil(read_until) + else: + reader = self._stream_reader.read(1024) + data_bytes = await asyncio.wait_for(reader, timeout=self._timeout) + data_dict = json.loads(data_bytes.decode('utf-8')) + return data_dict + except asyncio.TimeoutError as timeout_err: + _LOGGER.error('Recieved timeout whilst waiting for' + ' response from MyLink device.') + raise timeout_err + except UnicodeDecodeError as unicode_error: + _LOGGER.error('Could not decode Unicode: %s', data_bytes) + raise unicode_error + except json.decoder.JSONDecodeError as json_error: + _LOGGER.error('Could not decode JSON: %s', data_bytes) + raise json_error