Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bendews committed Nov 7, 2018
1 parent e81b3c5 commit 546fcb6
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ venv.bak/

# mypy
.mypy_cache/
.vscode
Pipfile*
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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='[email protected]',
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'
)
121 changes: 121 additions & 0 deletions somfy_mylink_synergy/__init__.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 546fcb6

Please sign in to comment.