-
-
Notifications
You must be signed in to change notification settings - Fork 36.3k
Add integration for iotty Smart Home #103073
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
Changes from 134 commits
18ab298
fc35303
3c25e8f
2b2022a
0f8be23
f6c9bb6
ae97d6f
213478f
2dfbcac
fdbef16
ac38fc5
cd56f98
f27bb4b
938a9af
31a8d01
fb65b75
63c4b2b
3525d64
2a3d779
d6a02a4
b5223a5
014c3e5
0903fbd
c719025
a7b9348
fea0e1d
4c77d3f
0895ce4
102cc67
852e63e
263b056
2446c28
a8171e6
42df504
a4bbf4d
fa97a76
41d1d5a
ab6afb1
1adc04d
a137732
def435b
2378530
a2c2e37
c72dec7
6b4ac81
f0d1c82
fd9d6b9
235d95d
57f93c7
d9c1e7c
e131b4f
e5657db
ddd9155
aa7d87c
f6e8269
2406cea
45993ed
ef16522
860cb78
fb520ad
b57182b
a6d9f43
fd14b35
9954782
b3e4195
dc47223
f3bf64b
5df2e39
85c85a7
7924c27
40dd193
90c5cb5
2874d26
0d67c1c
6c9054c
d8ac598
d5f447a
202bdd7
1c273e1
4c30c1a
eec1675
76b20bb
65317e0
c34e841
f364332
92b5cd9
3c07035
291c89d
7d68719
dec43e9
4953f33
3ab508c
d338e19
02551d6
e67903c
3df1fa7
14ecf78
9b56504
6bc5d16
2bf2cdd
6371d37
86b29da
84d9977
0be59ce
a3a32e0
d2989f2
fe9da43
4497c2c
d3b6cc5
56cf2cb
43e79c1
271e9e7
a47a715
b02eea8
02acc82
01f8759
7278fec
3328374
ce6e24e
4eac992
b6cbcff
4f14518
91cdb09
e87cead
43c2662
055f775
eb195a3
f07da58
b90f820
383fc90
c618e69
c4ac6bb
5e87f2a
1399c04
967e260
0b17b2c
299d884
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """The iotty integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
| import logging | ||
|
|
||
| from iottycloud.device import Device | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.config_entry_oauth2_flow import ( | ||
| OAuth2Session, | ||
| async_get_config_entry_implementation, | ||
| ) | ||
|
|
||
| from . import coordinator | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.SWITCH] | ||
|
|
||
| type IottyConfigEntry = ConfigEntry[IottyConfigEntryData] | ||
|
|
||
|
|
||
| @dataclass | ||
| class IottyConfigEntryData: | ||
| """Contains config entry data for iotty.""" | ||
|
|
||
| known_devices: set[Device] | ||
| coordinator: coordinator.IottyDataUpdateCoordinator | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: IottyConfigEntry) -> bool: | ||
| """Set up iotty from a config entry.""" | ||
| _LOGGER.debug("async_setup_entry entry_id=%s", entry.entry_id) | ||
|
|
||
| implementation = await async_get_config_entry_implementation(hass, entry) | ||
| session = OAuth2Session(hass, entry, implementation) | ||
|
|
||
| data_update_coordinator = coordinator.IottyDataUpdateCoordinator( | ||
| hass, entry, session | ||
| ) | ||
|
|
||
| entry.runtime_data = IottyConfigEntryData(set(), data_update_coordinator) | ||
|
|
||
| await data_update_coordinator.async_config_entry_first_refresh() | ||
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| """API for iotty bound to Home Assistant OAuth.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from aiohttp import ClientSession | ||
| from iottycloud.cloudapi import CloudApi | ||
|
|
||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers import config_entry_oauth2_flow | ||
|
|
||
| OAUTH2_CLIENT_ID = "hass-iotty" | ||
| IOTTYAPI_BASE = "https://homeassistant.iotty.com/" | ||
|
|
||
|
|
||
| class IottyProxy(CloudApi): | ||
| """Provide iotty authentication tied to an OAuth2 based config entry.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| hass: HomeAssistant, | ||
| websession: ClientSession, | ||
| oauth_session: config_entry_oauth2_flow.OAuth2Session, | ||
| ) -> None: | ||
| """Initialize iotty auth.""" | ||
|
|
||
| super().__init__(websession, IOTTYAPI_BASE, OAUTH2_CLIENT_ID) | ||
| if oauth_session is None: | ||
| raise ValueError("oauth_session") | ||
| self._oauth_session = oauth_session | ||
| self._hass = hass | ||
|
|
||
| async def async_get_access_token(self) -> Any: | ||
| """Return a valid access token.""" | ||
|
|
||
| if not self._oauth_session.valid_token: | ||
| await self._oauth_session.async_ensure_token_valid() | ||
|
|
||
| return self._oauth_session.token["access_token"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Application credentials platform for iotty.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from homeassistant.components.application_credentials import AuthorizationServer | ||
| from homeassistant.core import HomeAssistant | ||
|
|
||
| OAUTH2_AUTHORIZE = "https://auth.iotty.com/.auth/oauth2/login" | ||
| OAUTH2_TOKEN = "https://auth.iotty.com/.auth/oauth2/token" | ||
|
|
||
|
|
||
| async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: | ||
| """Return authorization server.""" | ||
| return AuthorizationServer( | ||
| authorize_url=OAUTH2_AUTHORIZE, | ||
| token_url=OAUTH2_TOKEN, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """Config flow for iotty.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
|
|
||
| from homeassistant.helpers import config_entry_oauth2_flow | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
|
|
||
| class OAuth2FlowHandler( | ||
| config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN | ||
| ): | ||
| """Config flow to handle iotty OAuth2 authentication.""" | ||
|
|
||
| DOMAIN = DOMAIN | ||
|
|
||
| @property | ||
| def logger(self) -> logging.Logger: | ||
| """Return logger.""" | ||
| return logging.getLogger(__name__) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Constants for the iotty integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| DOMAIN = "iotty" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| """DataUpdateCoordinator for iotty.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
| from datetime import timedelta | ||
| import logging | ||
|
|
||
| from iottycloud.device import Device | ||
| from iottycloud.verbs import RESULT, STATUS | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers import aiohttp_client, device_registry as dr | ||
| from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session | ||
| from homeassistant.helpers.entity import Entity | ||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||
|
|
||
| from . import api | ||
| from .const import DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| UPDATE_INTERVAL = timedelta(seconds=30) | ||
|
|
||
|
|
||
| @dataclass | ||
| class IottyData: | ||
| """iotty data stored in the DataUpdateCoordinator.""" | ||
|
|
||
| devices: list[Device] | ||
|
|
||
|
|
||
| class IottyDataUpdateCoordinator(DataUpdateCoordinator[IottyData]): | ||
| """Class to manage fetching Iotty data.""" | ||
|
|
||
| config_entry: ConfigEntry | ||
| _entities: dict[str, Entity] | ||
| _devices: list[Device] | ||
| _device_registry: dr.DeviceRegistry | ||
|
|
||
| def __init__( | ||
| self, hass: HomeAssistant, entry: ConfigEntry, session: OAuth2Session | ||
| ) -> None: | ||
| """Initialize the coordinator.""" | ||
| _LOGGER.debug("Initializing iotty data update coordinator") | ||
|
|
||
| super().__init__( | ||
| hass, | ||
| _LOGGER, | ||
| name=f"{DOMAIN}_coordinator", | ||
| update_interval=UPDATE_INTERVAL, | ||
| ) | ||
|
|
||
| self.config_entry = entry | ||
| self._entities = {} | ||
| self._devices = [] | ||
| self.iotty = api.IottyProxy( | ||
| hass, aiohttp_client.async_get_clientsession(hass), session | ||
| ) | ||
| self._device_registry = dr.async_get(hass) | ||
|
|
||
| async def async_config_entry_first_refresh(self) -> None: | ||
| """Override the first refresh to also fetch iotty devices list.""" | ||
| _LOGGER.debug("Fetching devices list from iottyCloud") | ||
| self._devices = await self.iotty.get_devices() | ||
emontnemery marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _LOGGER.debug("There are %d devices", len(self._devices)) | ||
|
|
||
| await super().async_config_entry_first_refresh() | ||
|
|
||
| async def _async_update_data(self) -> IottyData: | ||
| """Fetch data from iottyCloud device.""" | ||
| _LOGGER.debug("Fetching devices status from iottyCloud") | ||
|
|
||
| current_devices = await self.iotty.get_devices() | ||
|
|
||
| removed_devices = [ | ||
| d | ||
| for d in self._devices | ||
| if not any(x.device_id == d.device_id for x in current_devices) | ||
| ] | ||
|
|
||
| for removed_device in removed_devices: | ||
| device_to_remove = self._device_registry.async_get_device( | ||
| {(DOMAIN, removed_device.device_id)} | ||
| ) | ||
| if device_to_remove is not None: | ||
| self._device_registry.async_remove_device(device_to_remove.id) | ||
|
|
||
| self._devices = current_devices | ||
|
|
||
| for device in self._devices: | ||
| res = await self.iotty.get_status(device.device_id) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you make one request per device, I suggest creating a coordinator per device.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, I'm not sure if that's what you meant, but I have used the coordinator entity! Let me know if that satisfies your request
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What @edenhaus meant is that it seems the coordinator is not useful, because you make one API call for each device. Instead, each device could have its own coordinator.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for pointing that out. Actually, as you already said at the end, our future plans would actually benefit from having all the info in one coordinator. Moreover, we are also considering using a single API call to get all the devices info at once, and then cycle through them to get the updated info, which cannot be done using one coordinator per device. |
||
| json = res.get(RESULT, {}) | ||
| if ( | ||
| not isinstance(res, dict) | ||
| or RESULT not in res | ||
| or not isinstance(json := res[RESULT], dict) | ||
| or not (status := json.get(STATUS)) | ||
| ): | ||
| _LOGGER.warning("Unable to read status for device %s", device.device_id) | ||
| else: | ||
| _LOGGER.debug( | ||
| "Retrieved status: '%s' for device %s", status, device.device_id | ||
| ) | ||
| device.update_status(status) | ||
|
|
||
| return IottyData(self._devices) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "domain": "iotty", | ||
| "name": "iotty", | ||
| "codeowners": ["@pburgio"], | ||
| "config_flow": true, | ||
| "dependencies": ["application_credentials"], | ||
| "documentation": "https://www.home-assistant.io/integrations/iotty", | ||
| "integration_type": "device", | ||
| "iot_class": "cloud_polling", | ||
| "requirements": ["iottycloud==0.1.3"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "config": { | ||
| "step": { | ||
| "pick_implementation": { | ||
| "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" | ||
| } | ||
| }, | ||
| "abort": { | ||
| "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", | ||
| "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||
| "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", | ||
| "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", | ||
| "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", | ||
| "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", | ||
| "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" | ||
| }, | ||
| "create_entry": { | ||
| "default": "[%key:common::config_flow::create_entry::authenticated%]" | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.