forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add microBees integration (home-assistant#99573)
* Create a new homeassistan integration for microBees * black --fast homeassistant tests * Switch platform * rename folder * rename folder * Update owners * aiohttp removed in favor of hass * Update config_flow.py * Update __init__.py * Update const.py * Update manifest.json * Update string.json * Update servicesMicrobees.py * Update switch.py * Update __init__.py * Update it.json * Create a new homeassistan integration for microBees * black --fast homeassistant tests * Switch platform * rename folder * rename folder * Update owners * aiohttp removed in favor of hass * Update config_flow.py * Update __init__.py * Update const.py * Update manifest.json * Update string.json * Update servicesMicrobees.py * Update switch.py * Update __init__.py * Update it.json * fixes review * fixes review * fixes review * pyproject.toml * Update package_constraints.txt * fixes review * bug fixes * bug fixes * delete microbees connector * add other productID in switch * added coordinator and enanchments * added coordinator and enanchments * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * fixes from suggestions * add test * add test * add test * add test * requested commit * requested commit * requested commit * requested commit * reverting .strict-typing and added microbees to .coveragerc * remove log * remove log * remove log * remove log * add test for microbeesExeption and Exeption * add test for microbeesExeption and Exeption * add test for microbeesException and Exception * add test for microbeesException and Exception * add test for microbeesException and Exception --------- Co-authored-by: FedDam <noceracity@gmail.com> Co-authored-by: Federico D'Amico <48856240+FedDam@users.noreply.github.com>
- Loading branch information
1 parent
b349a46
commit 3a4c6fc
Showing
23 changed files
with
1,012 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
"""The microBees integration.""" | ||
|
||
from dataclasses import dataclass | ||
from http import HTTPStatus | ||
|
||
import aiohttp | ||
from microBeesPy.microbees import MicroBees | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_ACCESS_TOKEN | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | ||
from homeassistant.helpers import config_entry_oauth2_flow | ||
|
||
from .const import DOMAIN, PLATFORMS | ||
from .coordinator import MicroBeesUpdateCoordinator | ||
|
||
|
||
@dataclass(frozen=True, kw_only=True) | ||
class HomeAssistantMicroBeesData: | ||
"""Microbees data stored in the Home Assistant data object.""" | ||
|
||
connector: MicroBees | ||
coordinator: MicroBeesUpdateCoordinator | ||
session: config_entry_oauth2_flow.OAuth2Session | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up microBees from a config entry.""" | ||
implementation = ( | ||
await config_entry_oauth2_flow.async_get_config_entry_implementation( | ||
hass, entry | ||
) | ||
) | ||
|
||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) | ||
try: | ||
await session.async_ensure_token_valid() | ||
except aiohttp.ClientResponseError as ex: | ||
if ex.status in ( | ||
HTTPStatus.BAD_REQUEST, | ||
HTTPStatus.UNAUTHORIZED, | ||
HTTPStatus.FORBIDDEN, | ||
): | ||
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex | ||
raise ConfigEntryNotReady from ex | ||
microbees = MicroBees(token=session.token[CONF_ACCESS_TOKEN]) | ||
coordinator = MicroBeesUpdateCoordinator(hass, microbees) | ||
await coordinator.async_config_entry_first_refresh() | ||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantMicroBeesData( | ||
connector=microbees, | ||
coordinator=coordinator, | ||
session=session, | ||
) | ||
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.""" | ||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"""API for microBees bound to Home Assistant OAuth.""" | ||
|
||
from homeassistant.const import CONF_ACCESS_TOKEN | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers import config_entry_oauth2_flow | ||
|
||
|
||
class ConfigEntryAuth: | ||
"""Provide microBees authentication tied to an OAuth2 based config entry.""" | ||
|
||
def __init__( | ||
self, | ||
hass: HomeAssistant, | ||
oauth2_session: config_entry_oauth2_flow.OAuth2Session, | ||
) -> None: | ||
"""Initialize microBees Auth.""" | ||
self.oauth_session = oauth2_session | ||
self.hass = hass | ||
|
||
@property | ||
def access_token(self) -> str: | ||
"""Return the access token.""" | ||
return self.oauth_session.token[CONF_ACCESS_TOKEN] | ||
|
||
async def check_and_refresh_token(self) -> str: | ||
"""Check the token.""" | ||
await self.oauth_session.async_ensure_token_valid() | ||
return self.access_token |
14 changes: 14 additions & 0 deletions
14
homeassistant/components/microbees/application_credentials.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
"""application_credentials platform the microBees integration.""" | ||
|
||
from homeassistant.components.application_credentials import AuthorizationServer | ||
from homeassistant.core import HomeAssistant | ||
|
||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN | ||
|
||
|
||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: | ||
"""Return auth implementation.""" | ||
return AuthorizationServer( | ||
authorize_url=OAUTH2_AUTHORIZE, | ||
token_url=OAUTH2_TOKEN, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
"""Config flow for microBees integration.""" | ||
from collections.abc import Mapping | ||
import logging | ||
from typing import Any | ||
|
||
from microBeesPy.microbees import MicroBees, MicroBeesException | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class OAuth2FlowHandler( | ||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN | ||
): | ||
"""Handle a config flow for microBees.""" | ||
|
||
DOMAIN = DOMAIN | ||
reauth_entry: config_entries.ConfigEntry | None = None | ||
|
||
@property | ||
def logger(self) -> logging.Logger: | ||
"""Return logger.""" | ||
return logging.getLogger(__name__) | ||
|
||
@property | ||
def extra_authorize_data(self) -> dict[str, Any]: | ||
"""Extra data that needs to be appended to the authorize url.""" | ||
scopes = ["read", "write"] | ||
return {"scope": " ".join(scopes)} | ||
|
||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: | ||
"""Create an oauth config entry or update existing entry for reauth.""" | ||
|
||
microbees = MicroBees( | ||
session=aiohttp_client.async_get_clientsession(self.hass), | ||
token=data[CONF_TOKEN][CONF_ACCESS_TOKEN], | ||
) | ||
|
||
try: | ||
current_user = await microbees.getMyProfile() | ||
except MicroBeesException: | ||
return self.async_abort(reason="invalid_auth") | ||
except Exception: # pylint: disable=broad-except | ||
self.logger.exception("Unexpected error") | ||
return self.async_abort(reason="unknown") | ||
|
||
if not self.reauth_entry: | ||
await self.async_set_unique_id(current_user.id) | ||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry( | ||
title=current_user.username, | ||
data=data, | ||
) | ||
if self.reauth_entry.unique_id == current_user.id: | ||
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) | ||
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) | ||
return self.async_abort(reason="reauth_successful") | ||
return self.async_abort(reason="wrong_account") | ||
|
||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: | ||
"""Perform reauth upon an API authentication error.""" | ||
self.reauth_entry = self.hass.config_entries.async_get_entry( | ||
self.context["entry_id"] | ||
) | ||
return await self.async_step_reauth_confirm() | ||
|
||
async def async_step_reauth_confirm( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Confirm reauth dialog.""" | ||
if user_input is None: | ||
return self.async_show_form(step_id="reauth_confirm") | ||
return await self.async_step_user() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""Constants for the microBees integration.""" | ||
from homeassistant.const import Platform | ||
|
||
DOMAIN = "microbees" | ||
OAUTH2_AUTHORIZE = "https://dev.microbees.com/oauth/authorize" | ||
OAUTH2_TOKEN = "https://dev.microbees.com/oauth/token" | ||
PLATFORMS = [ | ||
Platform.SWITCH, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
"""The microBees Coordinator.""" | ||
|
||
import asyncio | ||
from dataclasses import dataclass | ||
from datetime import timedelta | ||
from http import HTTPStatus | ||
import logging | ||
|
||
import aiohttp | ||
from microBeesPy.microbees import Actuator, Bee, MicroBees, MicroBeesException | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
@dataclass | ||
class MicroBeesCoordinatorData: | ||
"""Microbees data from the Coordinator.""" | ||
|
||
bees: dict[int, Bee] | ||
actuators: dict[int, Actuator] | ||
|
||
|
||
class MicroBeesUpdateCoordinator(DataUpdateCoordinator[MicroBeesCoordinatorData]): | ||
"""MicroBees coordinator.""" | ||
|
||
def __init__(self, hass: HomeAssistant, microbees: MicroBees) -> None: | ||
"""Initialize microBees coordinator.""" | ||
super().__init__( | ||
hass, | ||
_LOGGER, | ||
name="microBees Coordinator", | ||
update_interval=timedelta(seconds=30), | ||
) | ||
self.microbees = microbees | ||
|
||
async def _async_update_data(self) -> MicroBeesCoordinatorData: | ||
"""Fetch data from API endpoint.""" | ||
async with asyncio.timeout(10): | ||
try: | ||
bees = await self.microbees.getBees() | ||
except aiohttp.ClientResponseError as err: | ||
if err.status is HTTPStatus.UNAUTHORIZED: | ||
raise ConfigEntryAuthFailed( | ||
"Token not valid, trigger renewal" | ||
) from err | ||
raise UpdateFailed(f"Error communicating with API: {err}") from err | ||
|
||
except MicroBeesException as err: | ||
raise UpdateFailed(f"Error communicating with API: {err}") from err | ||
|
||
bees_dict = {} | ||
actuators_dict = {} | ||
for bee in bees: | ||
bees_dict[bee.id] = bee | ||
for actuator in bee.actuators: | ||
actuators_dict[actuator.id] = actuator | ||
return MicroBeesCoordinatorData(bees=bees_dict, actuators=actuators_dict) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""Base entity for microBees.""" | ||
|
||
from microBeesPy.microbees import Actuator, Bee | ||
|
||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import DOMAIN | ||
from .coordinator import MicroBeesUpdateCoordinator | ||
|
||
|
||
class MicroBeesEntity(CoordinatorEntity[MicroBeesUpdateCoordinator]): | ||
"""Base class for microBees entities.""" | ||
|
||
_attr_has_entity_name = True | ||
|
||
def __init__( | ||
self, | ||
coordinator: MicroBeesUpdateCoordinator, | ||
bee_id: int, | ||
actuator_id: int, | ||
) -> None: | ||
"""Initialize the microBees entity.""" | ||
super().__init__(coordinator) | ||
self.bee_id = bee_id | ||
self.actuator_id = actuator_id | ||
self._attr_unique_id = f"{bee_id}_{actuator_id}" | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={(DOMAIN, str(bee_id))}, | ||
manufacturer="microBees", | ||
name=self.bee.name, | ||
model=self.bee.prototypeName, | ||
) | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Status of the bee.""" | ||
return ( | ||
super().available | ||
and self.bee_id in self.coordinator.data.bees | ||
and self.bee.active | ||
) | ||
|
||
@property | ||
def bee(self) -> Bee: | ||
"""Return the bee.""" | ||
return self.coordinator.data.bees[self.bee_id] | ||
|
||
@property | ||
def actuator(self) -> Actuator: | ||
"""Return the actuator.""" | ||
return self.coordinator.data.actuators[self.actuator_id] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"entity": { | ||
"switch": { | ||
"socket_eu": { | ||
"default": "mdi:power-socket-eu" | ||
}, | ||
"socket_it": { | ||
"default": "mdi:power-socket-it" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "microbees", | ||
"name": "microBees", | ||
"codeowners": ["@microBeesTech"], | ||
"config_flow": true, | ||
"dependencies": ["application_credentials"], | ||
"documentation": "https://www.home-assistant.io/integrations/microbees", | ||
"iot_class": "cloud_polling", | ||
"requirements": ["microBeesPy==0.2.5"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"pick_implementation": { | ||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" | ||
} | ||
}, | ||
"error": { | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"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%]", | ||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", | ||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", | ||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", | ||
"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%]" | ||
} | ||
} | ||
} |
Oops, something went wrong.