Skip to content

Commit

Permalink
Initial commit of new Python implementation
Browse files Browse the repository at this point in the history
commit 1de0940
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Thu Jul 23 22:21:56 2020 +0200

    Squashed commit of the following:

    commit 6caeb9a
    Author: Vincent Le Bourlot <vlebourl@gmail.com>
    Date:   Thu Jul 23 13:21:40 2020 +0200

        Round float state to 2 digits. (#164)

        * Round float state to 2 digits.

        Github Issue: #163

    commit e6b4c22
    Author: Vincent Le Bourlot <vlebourl@gmail.com>
    Date:   Thu Jul 23 11:50:11 2020 +0200

        Cover position was incorrectly inverted when 0 because bool(0) == False. (#165)

    commit 05cd609
    Author: Vincent Le Bourlot <vlebourl@gmail.com>
    Date:   Thu Jul 23 08:27:39 2020 +0200

        Enhancement/refactor climate (#159)

        * First commit for AtlanticElectricalHeater.
        Github Issue: #151

        * Refactored climate.
        * **Breaking** Removed options flow
        * **Breaking** Removed SUPPORT_TARGET_TEMPERATURE for AtlanticElectricalHeater IO component
        * Removed private properties.
        * Removed update methods.
        * Removed __init__ when possible.
        * Moved classes to their own files.
        * Add DimmerExteriorHeating

        Github Issue: #151

        * Fix/fix 161 (#162)

        Github Issue: #161

commit 4f1b0a7
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 14:46:13 2020 +0200

    Change debug text

commit f212bc6
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 14:22:44 2020 +0200

    Reference to the right DOMAINS

commit 67e09af
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 14:17:11 2020 +0200

    Reference to official domains

commit a1cf008
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 14:04:37 2020 +0200

    Add missing LOCK import

commit c971da9
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 14:00:06 2020 +0200

    Add constants instead of ugly strings

commit 22a1623
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 13:49:08 2020 +0200

    Change PLATFORMS to SUPPORTED_PLATFORMS

commit a57d246
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 13:08:12 2020 +0200

    Revert removal of DOMAIN

commit bf410ec
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 13:05:30 2020 +0200

    Remove unused imports

commit 4884a5f
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 13:02:27 2020 +0200

    Reformat switch.py

commit 01c1df1
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 12:59:19 2020 +0200

    Bugfix for switch

commit a13348f
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 12:48:39 2020 +0200

    Close client before removing client...

commit 15bb8cd
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 12:45:53 2020 +0200

    Make code more readable

commit d896200
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 12:43:59 2020 +0200

    Close HTTP client correctly

    Thanks @eavanvalkenburg!

commit 24b7190
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 10:18:31 2020 +0200

    Remove unnecessary loop

commit 28cd639
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 09:45:46 2020 +0200

    Refactor based on feedback

commit d3ccafe
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 01:07:08 2020 +0200

    Simply config flow and add better exception logging

commit c38f2e5
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 01:01:39 2020 +0200

    Only initialize platforms with devices and decide platform based on uiclass and widget

commit 4f579b8
Author: Mick Vleeshouwer <mick@imick.nl>
Date:   Tue Jul 21 00:31:28 2020 +0200

    Add async Python API wrapper
  • Loading branch information
iMicknl committed Jul 23, 2020
1 parent 6caeb9a commit 8e4b8b2
Show file tree
Hide file tree
Showing 16 changed files with 307 additions and 1,378 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -22,7 +22,7 @@ https://github.com/imicknl/ha-tahoma

## Supported devices

This component doesn't have a hardcoded list of devices anymore, but relies on the `uiclass` of every Somfy device. This way more devices will be supported out of the box, based on their category and available states and commands.
This component doesn't have a hardcoded list of devices anymore, but relies on the `ui_class` of every Somfy device. This way more devices will be supported out of the box, based on their category and available states and commands.

If your device is not supported, it will show the following message in the debug log. You can use this to create a new issue in the repository to see if the component can be added.

Expand Down
104 changes: 55 additions & 49 deletions custom_components/tahoma/__init__.py
@@ -1,30 +1,19 @@
"""The TaHoma integration."""
import asyncio
import json
from collections import defaultdict
import logging

from requests.exceptions import RequestException
from tahoma_api.client import TahomaClient
from tahoma_api.exceptions import BadCredentialsException, TooManyRequestsException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant

from .const import DOMAIN, TAHOMA_TYPES
from .tahoma_api import TahomaApi
from .const import DOMAIN, SUPPORTED_PLATFORMS, TAHOMA_TYPES

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [
"binary_sensor",
"climate",
"cover",
"light",
"lock",
"scene",
"sensor",
"switch",
]


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the TaHoma component."""
Expand All @@ -33,66 +22,83 @@ async def async_setup(hass: HomeAssistant, config: dict):

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up TaHoma from a config entry."""

hass.data.setdefault(DOMAIN, {})

username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)

try:
controller = await hass.async_add_executor_job(TahomaApi, username, password)
await hass.async_add_executor_job(controller.get_setup)
devices = await hass.async_add_executor_job(controller.get_devices)
scenes = await hass.async_add_executor_job(controller.get_action_groups)

# TODO Add better exception handling
except RequestException:
_LOGGER.exception("Error when getting devices from the TaHoma API")
client = TahomaClient(username, password)
await client.login()
except TooManyRequestsException as exception:
_LOGGER.exception(exception)
return False
except BadCredentialsException as exception:
_LOGGER.exception(exception)
return False
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
return False

devices = await client.get_devices()
scenes = await client.get_scenarios()

hass.data[DOMAIN][entry.entry_id] = {
"controller": controller,
"devices": [],
"scenes": [],
"controller": client,
"entities": defaultdict(list),
}

hass.data[DOMAIN][entry.entry_id]["entities"]["scene"] = scenes

for device in devices:
_device = controller.get_device(device)
if device.widget in TAHOMA_TYPES or device.ui_class in TAHOMA_TYPES:
platform = TAHOMA_TYPES.get(device.widget) or TAHOMA_TYPES.get(
device.ui_class
)

if _device.uiclass in TAHOMA_TYPES:
if TAHOMA_TYPES[_device.uiclass] in PLATFORMS:
component = TAHOMA_TYPES[_device.uiclass]
hass.data[DOMAIN][entry.entry_id]["devices"].append(_device)
if platform in SUPPORTED_PLATFORMS:
hass.data[DOMAIN][entry.entry_id]["entities"][platform].append(device)

elif _device.type not in [
"ogp:Bridge"
elif device.controllable_name not in [
"ogp:Bridge",
"internal:PodV2Component",
"internal:TSKAlarmComponent",
]: # Add here devices to hide from the debug log.
_LOGGER.debug(
"Unsupported Tahoma device (%s). Create an issue on Github with the following information. \n\n %s \n %s \n %s",
_device.type,
_device.type + " - " + _device.uiclass + " - " + _device.widget,
json.dumps(_device.command_def) + ",",
json.dumps(_device.states_def),
"Unsupported Tahoma device detected (%s - %s - %s).",
device.controllable_name,
device.ui_class,
device.widget,
)

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
entities_per_platform = hass.data[DOMAIN][entry.entry_id]["entities"]

for platform in entities_per_platform:
if len(entities_per_platform) > 0:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

for scene in scenes:
hass.data[DOMAIN][entry.entry_id]["scenes"].append(scene)
async def async_close_client(self, *_):
"""Close HTTP client."""
await client.close()

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_client)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""

client = hass.data[DOMAIN][entry.entry_id].get("controller")
await client.close()

unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
for component in SUPPORTED_PLATFORMS
]
)
)
Expand Down
14 changes: 5 additions & 9 deletions custom_components/tahoma/binary_sensor.py
Expand Up @@ -8,15 +8,13 @@
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_OPENING,
DEVICE_CLASS_SMOKE,
DOMAIN as BINARY_SENSOR,
BinarySensorEntity,
)
from homeassistant.const import STATE_OFF, STATE_ON

from .const import DOMAIN, TAHOMA_TYPES
from .const import DOMAIN
from .tahoma_device import TahomaDevice

_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(seconds=120)

CORE_BUTTON_STATE = "core:ButtonState"
Expand Down Expand Up @@ -64,10 +62,8 @@ async def async_setup_entry(hass, entry, async_add_entities):

entities = [
TahomaBinarySensor(device, controller)
for device in data.get("devices")
if TAHOMA_TYPES[device.uiclass] == "binary_sensor"
for device in data.get("entities").get(BINARY_SENSOR)
]

async_add_entities(entities)


Expand All @@ -93,8 +89,8 @@ def is_on(self):
def device_class(self):
"""Return the class of the device."""
return (
TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(self.tahoma_device.widget)
or TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(self.tahoma_device.uiclass)
TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(self.device.widget)
or TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(self.device.ui_class)
or None
)

Expand Down
8 changes: 4 additions & 4 deletions custom_components/tahoma/climate.py
@@ -1,9 +1,11 @@
"""Support for TaHoma climate devices."""

from homeassistant.components.climate import DOMAIN as CLIMATE

from .climate_aeh import AtlanticElectricalHeater
from .climate_deh import DimmerExteriorHeating
from .climate_st import SomfyThermostat
from .const import DOMAIN, TAHOMA_TYPES
from .const import DOMAIN

AEH = "AtlanticElectricalHeater"
ST = "SomfyThermostat"
Expand All @@ -16,9 +18,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
data = hass.data[DOMAIN][entry.entry_id]
controller = data.get("controller")

climate_devices = [
d for d in data.get("devices") if TAHOMA_TYPES[d.uiclass] == "climate"
]
climate_devices = [device for device in data.get("entities").get(CLIMATE)]

entities = []
for device in climate_devices:
Expand Down
60 changes: 16 additions & 44 deletions custom_components/tahoma/config_flow.py
@@ -1,14 +1,14 @@
"""Config flow for TaHoma integration."""
import logging

from requests.exceptions import RequestException
from tahoma_api.client import TahomaClient
from tahoma_api.exceptions import BadCredentialsException, TooManyRequestsException
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME

from .const import DOMAIN
from .tahoma_api import TahomaApi

_LOGGER = logging.getLogger(__name__)

Expand All @@ -17,30 +17,6 @@
)


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
username = data.get(CONF_USERNAME)
password = data.get(CONF_PASSWORD)

try:
await hass.async_add_executor_job(TahomaApi, username, password)

except RequestException:
_LOGGER.exception("Error when trying to log in to the TaHoma API")
raise CannotConnect

# If you cannot connect:
# throw CannotConnect
# If the authentication is wrong:
# InvalidAuth

# Return info that you want to store in the config entry.
return {"title": username}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for TaHoma."""

Expand All @@ -52,30 +28,26 @@ async def async_step_user(self, user_input=None):
errors = {}

if user_input is not None:
unique_id = user_input.get(CONF_USERNAME)
await self.async_set_unique_id(unique_id)
username = user_input.get(CONF_USERNAME)
password = user_input.get(CONF_PASSWORD)

await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()

client = TahomaClient(username, password)

try:
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info["title"], data=user_input)
await client.login()
return self.async_create_entry(title=username, data=user_input)

except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
except TooManyRequestsException:
errors["base"] = "too_many_requests"
except BadCredentialsException:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
errors["base"] = "unknown"

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

0 comments on commit 8e4b8b2

Please sign in to comment.