Somfy Tahoma Official API


#41

Can you see an error in the Home Assistan log page or within the home-assistant.log file?


#42

@tetienne

Log Details (ERROR)
Thu Dec 20 2018 17:28:06 GMT+0100 (CET)

Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web_protocol.py", line 390, in start
    resp = await self._request_handler(request)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web_app.py", line 366, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web_middlewares.py", line 106, in impl
    return await handler(request)
  File "/usr/local/lib/python3.6/site-packages/homeassistant/components/http/static.py", line 66, in staticresource_middleware
    return await handler(request)
  File "/usr/local/lib/python3.6/site-packages/homeassistant/components/http/real_ip.py", line 34, in real_ip_middleware
    return await handler(request)
  File "/usr/local/lib/python3.6/site-packages/homeassistant/components/http/ban.py", line 67, in ban_middleware
    return await handler(request)
  File "/usr/local/lib/python3.6/site-packages/homeassistant/components/http/auth.py", line 99, in auth_middleware
    return await handler(request)
  File "/usr/local/lib/python3.6/site-packages/homeassistant/components/http/view.py", line 115, in handle
    result = handler(request, **request.match_info)
  File "/config/custom_components/somfy/__init__.py", line 111, in get
    self.request_token(str(request.url))
  File "/config/deps/lib/python3.6/site-packages/pymfy/api/somfy_api.py", line 49, in request_token
    client_secret=self.client_secret)
  File "/usr/local/lib/python3.6/site-packages/requests_oauthlib/oauth2_session.py", line 187, in fetch_token
    state=self._state)
  File "/usr/local/lib/python3.6/site-packages/oauthlib/oauth2/rfc6749/clients/web_application.py", line 174, in parse_request_uri_response
    response = parse_authorization_code_response(uri, state=state)
  File "/usr/local/lib/python3.6/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 221, in parse_authorization_code_response
    raise InsecureTransportError()
oauthlib.oauth2.rfc6749.errors.InsecureTransportError: (insecure_transport) OAuth 2 MUST utilize https.

#43

Apparently it seems you have configured a non https URL in your configuration or within the Somfy developper portal.
I have updated component on my repo, you can download the new version if you want.


#44
###  Somfy  ###
somfy:
  client_id: !secret somfy_id
  client_secret: !secret somfy_secret

I have this in my config, do I need more?


#45

To avoid noise on this topic, I sent you a PM.


#46

Hi, I’ve added the support for the tilt position with my last changes: https://github.com/tetienne/home-assistant/commit/8b1231b9e238783b6aa3719f066002391938f34b. So once my component available you will be able to test :wink: There is one possible issue, home assistant use the value 100 when the blind is fully opened, I don’t know if it’s the same thing for Somfy. So if I’m lucky we are good, otherwise the command will be reverse.


#47

Hello, can You Tell me, how can i install it to hassio with raspberry? I realy want try to tilt. :slight_smile:


#48

Thx to @gieljnssns I was able to fix an issue with my component related to the the caddy addon (reverse proxy).

@Bojkas For the moment you will have to create a custom component. Create a file __init__.py at /config/custom_components/somfy with this content and a file somfy.py at /config/custom_components/cover with this content.

On the Somfy API website, log in using your Tahoma/Conexoon account. Open the My Apps menu and Add a new App where the redirect URI is <your HASS URL>/auth/somfy/callback

Update your configuration.yaml file with

http:
  base_url: <your HASS url>

somfy:
  client_id: <Consumer Key>
  client_secret: <Consumer Secret>

Reboot HASS. Back to the home page you will a notification message inviting you to click on a link. Once Somfy approval done, you will have access to your covers.

@pbavinck Can you have a look too?

You can follow the progress of this component looking at the pull request on home assistant repository.


Implementation of Somfy Connexoon IO
#49

Hello,

i do exactly what you wrote, but:

From log
Setup failed for somfy: Component not found.
Unable to find component somfy

In notification start screen
the following components and platforms could not be set up

I created file
/config/custom_components/somfy/somfy.py
and
/config/custom_components/cover/init.py

Hassio created pycache/sonoff.cpython-36.pyc

Configuration validation:
Component not found: somfy

Any idea?


#50

You created the files at the wrong path. You switched them. Double check my post.


#52

And you also have to changes this 2 lines

# from homeassistant.components.somfy import DOMAIN, SomfyEntity
from custom_components.somfy import DOMAIN, SomfyEntity

in the /config/custom_components/cover/somfy.py file


#53

I wrote bad. In folder somfy have init.py and in cover folder have somfy.py .

gieljnssns ok, i try IT.


#54

And what homeassistant.components.cover import CoverDevice, ATTR_POSITION,
ATTR_TILT_POSITION is ok?

Edit: doesnt work, still component not found. :frowning:


#55

Hi everyone. First, many thanks @tetienne for your great work.
I read every post of this thread, trying to make this working on my config (homeassistant in Docker on Synology NAS).
I registered on Somy Dev site and made a new App (and tested it with the online check from Somfy)
I installed pymfy 0.4.3, i made the folders and files for the custom components, i changed the “from” section of Somfy.py …
but i’m in the same case of Bojkas
2018-12-26 16:23:05 ERROR (MainThread) [homeassistant.loader] Unable to find component somfy
2018-12-26 16:23:05 ERROR (MainThread) [homeassistant.setup] Setup failed for somfy: Component not found.

One thing is that my HomeAssistant is not publicly available, so the Callback URL is not resolvable from the internet side.
I would like to help you make this component perfect for somfy users.
Is there a way to have more detailed log in HA ?
Arnaud
PS : i’m also French @tetienne but for universality it seems obvious that we need to exchange in english :wink:


#56

@koomik (Bonjour Arnaud :wink: )I’m glad to see your are interest by this component. If a lot of people use it, it will send to Somfy a good message and show them they did the good choice by creating this API. By the way, which devices do you have? Only cover?
About your question, the initial setup need your installation to be accessible from the outside. This is due to the Oauth2 protocol. But I can see a workaround. You will have to create a file .somfy in your configuration folder which look like.

{"scope": ["user.basic", "api.full", "oa.site", "oa.user", "oa.device", "oa.devicedefinition", "level.0"], "access_token": "xxxxxxx", "token_type": "bearer", "expires_in": 3600, "expires_at": 1545863003.3705792, "refresh_token": "yyyyyy"}

@koomik @Bojkas Can you show me the full path of the two files I gave you? I tested from scratch on a fresh home assistant installation without any issue. Did you apply the @gieljnssns’s remark?


#57

config/custom_components/cover/somfy.py
config/custom_components/somfy/init.py
Config folder Is that with config.yaml

somfy.py
“”"
Support for Somfy Covers.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.somfy/
"""

from homeassistant.components.cover import CoverDevice, ATTR_POSITION, \
    ATTR_TILT_POSITION
from custom_components.somfy import DOMAIN, SomfyEntity, DEVICES

DEPENDENCIES = ['somfy']


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up the Somfy cover platform."""

    from pymfy.api.devices.category import Category
    categories = {Category.ROLLER_SHUTTER.value, Category.INTERIOR_BLIND.value,
                  Category.EXTERIOR_BLIND.value}

    devices = hass.data[DOMAIN][DEVICES]
    for cover in devices:
        if categories & set(cover.categories):
            add_entities([SomfyCover(cover, hass)])


class SomfyCover(SomfyEntity, CoverDevice):
    """Representation of a Somfy cover device."""

    def close_cover(self, **kwargs):
        """Close the cover."""
        from pymfy.api.devices.roller_shutter import RollerShutter
        RollerShutter(self.device, self.api).close()

    def open_cover(self, **kwargs):
        """Open the cover."""
        from pymfy.api.devices.roller_shutter import RollerShutter
        RollerShutter(self.device, self.api).open()

    def stop_cover(self, **kwargs):
        """Stop the cover"""
        from pymfy.api.devices.roller_shutter import RollerShutter
        RollerShutter(self.device, self.api).stop()

    def set_cover_position(self, **kwargs):
        """Move the cover shutter to a specific position."""
        position = kwargs.get(ATTR_POSITION)
        from pymfy.api.devices.roller_shutter import RollerShutter
        RollerShutter(self.device, self.api).set_position(100 - position)

    @property
    def current_cover_position(self):
        """Return the current position of cover shutter."""
        position = None
        try:
            from pymfy.api.devices.roller_shutter import RollerShutter
            shutter = RollerShutter(self.device, self.api)
            position = 100 - shutter.get_position()
        except StopIteration:
            pass
        return position

    @property
    def is_closed(self):
        """Return if the cover is closed."""
        is_closed = None
        try:
            from pymfy.api.devices.roller_shutter import RollerShutter
            is_closed = RollerShutter(self.device, self.api).is_closed()
        except StopIteration:
            pass
        return is_closed

    @property
    def current_cover_tilt_position(self):
        """Return current position of cover tilt.

        None is unknown, 0 is closed, 100 is fully open.
        """
        orientation = None
        try:
            from pymfy.api.devices.blind import Blind
            orientation = Blind(self.device, self.api).orientation
        except StopIteration:
            pass
        return orientation

    def set_cover_tilt_position(self, **kwargs):
        """Move the cover tilt to a specific position."""
        orientation = kwargs.get(ATTR_TILT_POSITION)
        from pymfy.api.devices.blind import Blind
        Blind(self.device, self.api).orientation = orientation

    def open_cover_tilt(self, **kwargs):
        """Open the cover tilt."""
        from pymfy.api.devices.blind import Blind
        Blind(self.device, self.api).orientation = 100

    def close_cover_tilt(self, **kwargs):
        """Close the cover tilt."""
        from pymfy.api.devices.blind import Blind
        Blind(self.device, self.api).orientation = 0

    def stop_cover_tilt(self, **kwargs):
        """Stop the cover."""
        from pymfy.api.devices.blind import Blind
        Blind(self.device, self.api).stop()

init.py
“”"
Support for Somfy hubs.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/somfy/
"""
import logging
from datetime import timedelta

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle

API = 'api'

DEVICES = 'devices'

REQUIREMENTS = ['pymfy==0.4.3']

_LOGGER = logging.getLogger(__name__)

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)

DOMAIN = 'somfy'

CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'

NOTIFICATION_CB_ID = 'somfy_cb_notification'
NOTIFICATION_OK_ID = 'somfy_ok_notification'
NOTIFICATION_TITLE = 'Somfy Setup'

ATTR_ACCESS_TOKEN = 'access_token'
ATTR_REFRESH_TOKEN = 'refresh_token'
ATTR_CLIENT_ID = 'client_id'
ATTR_CLIENT_SECRET = 'client_secret'

SOMFY_AUTH_CALLBACK_PATH = '/auth/somfy/callback'
SOMFY_AUTH_START = '/auth/somfy'

DEFAULT_CACHE_PATH = '.somfy'

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_CLIENT_ID): cv.string,
        vol.Required(CONF_CLIENT_SECRET): cv.string
    })
}, extra=vol.ALLOW_EXTRA)

SOMFY_COMPONENTS = ['cover']


def setup(hass, config):
    """Set up the Somfy component."""
    from pymfy.api.somfy_api import SomfyApi

    hass.data[DOMAIN] = {}

    # This is called to create the redirect so the user can Authorize Home .
    redirect_uri = '{}{}'.format(
        hass.config.api.base_url, SOMFY_AUTH_CALLBACK_PATH)
    conf = config[DOMAIN]
    api = SomfyApi(conf.get(CONF_CLIENT_ID),
                   conf.get(CONF_CLIENT_SECRET),
                   redirect_uri, hass.config.path(DEFAULT_CACHE_PATH))
    hass.data[DOMAIN][API] = api

    if not api.token:
        authorization_url, _ = api.get_authorization_url()
        hass.components.persistent_notification.create(
            'In order to authorize Home Assistant to view your Somfy devices'
            ' you must visit this <a href="{}" target="_blank">link</a>.'
            .format(authorization_url),
            title=NOTIFICATION_TITLE,
            notification_id=NOTIFICATION_CB_ID
        )
        hass.http.register_view(SomfyAuthCallbackView(config))
    else:
        update_all_devices(hass)
        for component in SOMFY_COMPONENTS:
            discovery.load_platform(hass, component, DOMAIN, {}, config)

    return True


class SomfyAuthCallbackView(HomeAssistantView):
    """Handle OAuth finish callback requests."""

    url = SOMFY_AUTH_CALLBACK_PATH
    name = 'auth:somfy:callback'
    requires_auth = False

    def __init__(self, config):
        """Initialize the OAuth callback view."""
        self.config = config

    @callback
    def get(self, request):
        """Finish OAuth callback request."""
        from aiohttp import web
        from oauthlib.oauth2 import MismatchingStateError
        from oauthlib.oauth2 import InsecureTransportError

        hass = request.app['hass']
        response = web.HTTPFound('/')

        try:
            code = request.query.get('code')
            hass.data[DOMAIN][API].request_token(code=code)
            hass.async_add_job(setup, hass, self.config)
            hass.components.persistent_notification.dismiss(NOTIFICATION_CB_ID)
            hass.components.persistent_notification.create(
                "Somfy has been successfully authorized!",
                title=NOTIFICATION_TITLE,
                notification_id=NOTIFICATION_CB_ID
            )
        except MismatchingStateError:
            _LOGGER.error("OAuth state not equal in request and response.",
                          exc_info=True)
        except InsecureTransportError:
            _LOGGER.error("Somfy redirect URI %s is insecure.", request.url,
                          exc_info=True)

        return response


class SomfyEntity(Entity):
    """Representation of a generic Somfy device."""

    def __init__(self, device, hass):
        """Initialize the Somfy device."""
        self.hass = hass
        self.device = device
        self.api = hass.data[DOMAIN][API]

    @property
    def unique_id(self):
        """Return the unique id base on the id returned by Somfy."""
        return self.device.id

    @property
    def name(self):
        """Return the name of the device."""
        return self.device.name

    def update(self):
        """Update the device with the latest data."""
        update_all_devices(self.hass)
        devices = self.hass.data[DOMAIN][DEVICES]
        self.device = next((d for d in devices if d.id == self.device.id),
                           self.device)


@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update_all_devices(hass):
    """Update all the devices."""
    from requests import HTTPError
    try:
        data = hass.data[DOMAIN]
        data[DEVICES] = data[API].get_devices()
    except HTTPError:
        _LOGGER.warning("Cannot update devices.", exc_info=True)

#58

__init__.py


#59

File must be init.py? No init.py?


#61

Yeeeehaaaaa
image

So, many thanks @gieljnssns for pointing us that init.py need to be with 2 underscores before and after the “init”
@tetienne for information, i’ve put the internal URL of my HA setup in the callback URL and it seems to work.
I’ll test it now


#62

That’s great. Sorry for my typo. I think I was tired by the Christmas meal :santa: I’ve updated my post.
Nice to see it can work also when your instance is offline. Tell me if you see any error in the log or a strange behavior. It can be due to my code or a bug on Somfy side.