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

Add microBees integration #99573

Merged
merged 93 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
aed16d1
Create a new homeassistan integration for microBees
FedDam Sep 4, 2023
65df20e
black --fast homeassistant tests
FedDam Sep 4, 2023
3d05e43
Switch platform
FedDam Sep 4, 2023
94a3c7d
rename folder
FedDam Sep 4, 2023
9f232cc
rename folder
FedDam Sep 4, 2023
e756caa
Update owners
marcolettieri Sep 4, 2023
070c583
Merge branch 'dev' into dev
marcolettieri Sep 5, 2023
96231b7
aiohttp removed in favor of hass
FedDam Sep 7, 2023
9a763b1
Merge branch 'dev' of https://github.com/microBeesTech/homeassistant …
FedDam Sep 7, 2023
bd9d2db
Merge branch 'dev' into dev
marcolettieri Sep 8, 2023
217a106
Update config_flow.py
FedDam Oct 12, 2023
6b354d1
Update __init__.py
FedDam Oct 12, 2023
8c3e048
Update const.py
FedDam Oct 12, 2023
897b63c
Update manifest.json
FedDam Oct 12, 2023
5df472b
Update string.json
FedDam Oct 12, 2023
8d73ff1
Update servicesMicrobees.py
FedDam Oct 12, 2023
ff33f00
Update switch.py
FedDam Oct 12, 2023
08a6d28
Update __init__.py
FedDam Oct 12, 2023
54ca8a6
Update it.json
FedDam Oct 12, 2023
5f5b06d
Merge branch 'home-assistant:dev' into dev
FedDam Jan 8, 2024
9c605b4
Create a new homeassistan integration for microBees
FedDam Sep 4, 2023
f61bfaf
black --fast homeassistant tests
FedDam Sep 4, 2023
7a81a2b
Switch platform
FedDam Sep 4, 2023
83fdbd5
rename folder
FedDam Sep 4, 2023
90fc8cf
rename folder
FedDam Sep 4, 2023
f5517f3
Update owners
marcolettieri Sep 4, 2023
0451f5d
aiohttp removed in favor of hass
FedDam Sep 7, 2023
e5d5b8b
Update config_flow.py
FedDam Oct 12, 2023
1ef2b85
Update __init__.py
FedDam Oct 12, 2023
f98d0b9
Update const.py
FedDam Oct 12, 2023
6588370
Update manifest.json
FedDam Oct 12, 2023
7cdb7ee
Update string.json
FedDam Oct 12, 2023
0df28fc
Update servicesMicrobees.py
FedDam Oct 12, 2023
dd5f294
Update switch.py
FedDam Oct 12, 2023
dd7cee8
Update __init__.py
FedDam Oct 12, 2023
7a8207d
Update it.json
FedDam Oct 12, 2023
48903b6
Merge branch 'dev' of https://github.com/microBeesTech/homeassistant …
FedDam Jan 9, 2024
630311d
Merge branch 'home-assistant:dev' into dev
FedDam Jan 9, 2024
49bd1d1
Merge branch 'home-assistant:dev' into dev
FedDam Jan 9, 2024
33edd81
Merge branch 'home-assistant:dev' into dev
FedDam Jan 11, 2024
fe1cdb5
Merge branch 'home-assistant:dev' into dev
marcolettieri Jan 11, 2024
19e050c
Merge branch 'dev' into dev
FedDam Jan 15, 2024
657da9e
Merge branch 'home-assistant:dev' into dev
FedDam Jan 16, 2024
fbb8f90
Merge branch 'dev' into dev
FedDam Jan 22, 2024
5fad65f
Merge branch 'dev' into dev
FedDam Jan 22, 2024
4457298
fixes review
FedDam Jan 22, 2024
61dc170
Merge branch 'dev' into dev
FedDam Jan 22, 2024
6706dc0
Merge branch 'dev' into dev
FedDam Jan 23, 2024
1fc1464
Merge branch 'home-assistant:dev' into dev
FedDam Jan 24, 2024
5fde300
fixes review
FedDam Jan 24, 2024
b5d871a
fixes review
FedDam Jan 24, 2024
e613f8a
pyproject.toml
FedDam Jan 24, 2024
ecc1c57
Update package_constraints.txt
FedDam Jan 24, 2024
bf65a12
fixes review
FedDam Jan 26, 2024
01cab63
Merge remote-tracking branch 'refs/remotes/origin/microbees' into mic…
FedDam Jan 26, 2024
3c98298
Merge pull request #1 from microBeesTech/microbees
marcolettieri Jan 26, 2024
d48374c
bug fixes
FedDam Jan 30, 2024
204cc99
bug fixes
FedDam Jan 30, 2024
b85414c
delete microbees connector
FedDam Feb 2, 2024
1230bb9
add other productID in switch
FedDam Feb 5, 2024
25af133
added coordinator and enanchments
FedDam Feb 8, 2024
7a22440
added coordinator and enanchments
FedDam Feb 8, 2024
18f064a
fixes from suggestions
FedDam Feb 8, 2024
6c34e92
fixes from suggestions
FedDam Feb 8, 2024
07dcb7b
fixes from suggestions
FedDam Feb 8, 2024
46fa844
fixes from suggestions
FedDam Feb 8, 2024
32c2daa
fixes from suggestions
FedDam Feb 8, 2024
ddd04a2
fixes from suggestions
FedDam Feb 8, 2024
2194566
fixes from suggestions
FedDam Feb 8, 2024
6904193
fixes from suggestions
FedDam Feb 9, 2024
255f6b8
fixes from suggestions
FedDam Feb 9, 2024
1cf3d9f
fixes from suggestions
FedDam Feb 9, 2024
4e3138f
fixes from suggestions
FedDam Feb 9, 2024
934c007
Merge branch 'dev' into dev
marcolettieri Feb 9, 2024
84db301
fixes from suggestions
FedDam Feb 9, 2024
dc531b9
add test
FedDam Feb 13, 2024
0ead78f
add test
FedDam Feb 15, 2024
79bb9e7
add test
FedDam Feb 15, 2024
67b1445
add test
FedDam Feb 16, 2024
250c45c
requested commit
FedDam Feb 16, 2024
168e685
requested commit
FedDam Feb 16, 2024
bf26a4e
requested commit
FedDam Feb 16, 2024
9d9cfb8
requested commit
FedDam Feb 16, 2024
c91ce62
reverting .strict-typing and added microbees to .coveragerc
FedDam Feb 16, 2024
5814212
remove log
FedDam Feb 19, 2024
b420c13
remove log
FedDam Feb 19, 2024
0c0536e
remove log
FedDam Feb 19, 2024
cbaca33
remove log
FedDam Feb 19, 2024
604bc6a
add test for microbeesExeption and Exeption
FedDam Feb 19, 2024
b03e217
add test for microbeesExeption and Exeption
FedDam Feb 19, 2024
3b2aa14
add test for microbeesException and Exception
FedDam Feb 19, 2024
2db964b
add test for microbeesException and Exception
FedDam Feb 19, 2024
c952972
add test for microbeesException and Exception
FedDam Feb 19, 2024
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
7 changes: 7 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,13 @@ omit =
homeassistant/components/meteoclimatic/__init__.py
homeassistant/components/meteoclimatic/sensor.py
homeassistant/components/meteoclimatic/weather.py
homeassistant/components/microbees/__init__.py
homeassistant/components/microbees/api.py
homeassistant/components/microbees/application_credentials.py
homeassistant/components/microbees/const.py
homeassistant/components/microbees/coordinator.py
homeassistant/components/microbees/entity.py
homeassistant/components/microbees/switch.py
homeassistant/components/microsoft/tts.py
homeassistant/components/mikrotik/hub.py
homeassistant/components/mill/climate.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,8 @@ build.json @home-assistant/supervisor
/tests/components/meteoclimatic/ @adrianmo
/homeassistant/components/metoffice/ @MrHarcombe @avee87
/tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/microbees/ @microBeesTech
/tests/components/microbees/ @microBeesTech
/homeassistant/components/mikrotik/ @engrbm87
/tests/components/mikrotik/ @engrbm87
/homeassistant/components/mill/ @danielhiversen
Expand Down
64 changes: 64 additions & 0 deletions homeassistant/components/microbees/__init__.py
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
28 changes: 28 additions & 0 deletions homeassistant/components/microbees/api.py
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:
Copy link
Member

@MartinHjelmare MartinHjelmare Apr 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't used anywhere. The idea with this class is that it should inherit the library client class and help the library to refresh the access token since Home Assistant is in control of it instead of the library.

https://developers.home-assistant.io/docs/api_lib_auth#oauth2

The only time the token is refreshed currently is when the config entry is setup. So the user needs to reload the config entry to refresh the token. This is not how it's supposed to work.

Before every request the client should call async_get_access_token (check_and_refresh_token) to get a valid token.

"""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 homeassistant/components/microbees/application_credentials.py
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,
)
77 changes: 77 additions & 0 deletions homeassistant/components/microbees/config_flow.py
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")
marcolettieri marked this conversation as resolved.
Show resolved Hide resolved
marcolettieri marked this conversation as resolved.
Show resolved Hide resolved

if not self.reauth_entry:
await self.async_set_unique_id(current_user.id)
marcolettieri marked this conversation as resolved.
Show resolved Hide resolved
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()
9 changes: 9 additions & 0 deletions homeassistant/components/microbees/const.py
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,
]
61 changes: 61 additions & 0 deletions homeassistant/components/microbees/coordinator.py
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)
52 changes: 52 additions & 0 deletions homeassistant/components/microbees/entity.py
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]
12 changes: 12 additions & 0 deletions homeassistant/components/microbees/icons.json
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"
}
}
}
}
10 changes: 10 additions & 0 deletions homeassistant/components/microbees/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "microbees",
"name": "microBees",
marcolettieri marked this conversation as resolved.
Show resolved Hide resolved
"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"]
}
28 changes: 28 additions & 0 deletions homeassistant/components/microbees/strings.json
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%]"
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're missing strings for abort reasons for invalid_auth, unknown, and wrong_account.

"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
}
}
Loading