Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make all functions async, utilise new Python wrapper, better exception handling, improved device mapping #160

Merged
merged 8 commits into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
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
109 changes: 59 additions & 50 deletions custom_components/tahoma/__init__.py
Original file line number Diff line number Diff line change
@@ -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,69 +22,89 @@ 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 scene in scenes:
hass.data[DOMAIN][entry.entry_id]["scenes"].append(scene)
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)
)

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()

entities_per_platform = hass.data[DOMAIN][entry.entry_id]["entities"]

unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in entities_per_platform
]
)
)

if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

Expand Down
14 changes: 5 additions & 9 deletions custom_components/tahoma/binary_sensor.py
Original file line number Diff line number Diff line change
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
10 changes: 5 additions & 5 deletions custom_components/tahoma/climate.py
Original file line number Diff line number Diff line change
@@ -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,16 +18,14 @@ 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:
if device.widget == AEH:
entities.append(AtlanticElectricalHeater(device, controller))
elif device.widget == ST:
base_url = device.url.split("#", 1)[0]
base_url = device.deviceurl.split("#", 1)[0]
sensor = None
entity_registry = await hass.helpers.entity_registry.async_get_registry()
for k, v in entity_registry.entities.items():
Expand Down
63 changes: 19 additions & 44 deletions custom_components/tahoma/config_flow.py
Original file line number Diff line number Diff line change
@@ -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,29 @@ 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()
await client.close()
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
errors["base"] = "unknown"
_LOGGER.exception(exception)

await client.close()

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."""
Loading