pax_global_header00006660000000000000000000000064135020475040014512gustar00rootroot0000000000000052 comment=814ae3f379f4c962cf0754115c46a78f913ea4fe bendews-somfy-mylink-synergy-814ae3f/000077500000000000000000000000001350204750400177105ustar00rootroot00000000000000bendews-somfy-mylink-synergy-814ae3f/.gitignore000066400000000000000000000023041350204750400216770ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .vscode Pipfile* bendews-somfy-mylink-synergy-814ae3f/LICENSE000066400000000000000000000020511350204750400207130ustar00rootroot00000000000000MIT License Copyright (c) 2018 Ben Dews Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bendews-somfy-mylink-synergy-814ae3f/README.md000066400000000000000000000025701350204750400211730ustar00rootroot00000000000000[![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.5.2 ## Usage ```python import asyncio from somfy_mylink_synergy import SomfyMyLinkSynergy loop = asyncio.get_event_loop() mylink = SomfyMyLinkSynergy('YourSystemID', '10.1.1.50') mylink_covers = loop.run_until_complete(mylink.status_info()) for device in mylink_covers['result']: print(device['targetID'], device['name']) # ('CC0000A.1', 'Bedroom Cover') # ('CC0000A.2', 'Kitchen Cover') mylink_scenes = loop.run_until_complete(mylink.scene_list()) for scene in mylink_scenes['result']: print(scene['sceneID'], scene['name']) # ('123456789', 'Morning') # ('987654321', 'Evening') mylink_ping = loop.run_until_complete(mylink.status_ping()) for device in mylink_ping['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)bendews-somfy-mylink-synergy-814ae3f/setup.py000066400000000000000000000011631350204750400214230ustar00rootroot00000000000000""" 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.4', 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' ) bendews-somfy-mylink-synergy-814ae3f/somfy_mylink_synergy/000077500000000000000000000000001350204750400242105ustar00rootroot00000000000000bendews-somfy-mylink-synergy-814ae3f/somfy_mylink_synergy/__init__.py000066400000000000000000000126231350204750400263250ustar00rootroot00000000000000import json import logging import asyncio import re 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 self._stream_ready = asyncio.Event() self._stream_ready.set() 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.""" # Wait for stream to be ready await self._stream_ready.wait() self._stream_ready.clear() _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) await self._stream_writer.drain() except TypeError as data_error: _LOGGER.error('Invalid data sent to device') raise data_error async def _close_socket(self): """Close Socket connection.""" self._stream_writer.close() await asyncio.sleep(0) # Mark stream as ready self._stream_ready.set() async def _recieve_data(self, read_until=None): """Recieve Data from MyLink using JsonRPC via Socket.""" 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_text = data_bytes.decode('utf-8') if "keepalive" in data_text: data_text = re.sub('({[a-zA-Z.\":]*keepalive.*?})', '', data_text) data_dict = json.loads(data_text) 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 finally: await self._close_socket()