Skip to content

Commit

Permalink
Split august and yale integrations (home-assistant#124677)
Browse files Browse the repository at this point in the history
* Split august and yale integrations [part 1] (home-assistant#122253)

* merge with dev

* Remove unused constant

* Remove yale IPv6 workaround (home-assistant#123409)

* Convert yale to use oauth (home-assistant#123806)

Co-authored-by: Joost Lekkerkerker <[email protected]>

* Update yale for switch from pubnub to websockets (home-assistant#124675)

---------

Co-authored-by: Joost Lekkerkerker <[email protected]>
  • Loading branch information
bdraco and joostlek authored Aug 28, 2024
1 parent edad766 commit 03ead27
Show file tree
Hide file tree
Showing 72 changed files with 5,811 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1660,6 +1660,8 @@ build.json @home-assistant/supervisor
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
/homeassistant/components/xiaomi_tv/ @simse
/homeassistant/components/xmpp/ @fabaff @flowolf
/homeassistant/components/yale/ @bdraco
/tests/components/yale/ @bdraco
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
/tests/components/yale_smart_alarm/ @gjohansson-ST
/homeassistant/components/yalexs_ble/ @bdraco
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/brands/yale.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"domain": "yale",
"name": "Yale",
"integrations": ["august", "yale_smart_alarm", "yalexs_ble", "yale_home"]
"integrations": [
"august",
"yale_smart_alarm",
"yalexs_ble",
"yale_home",
"yale"
]
}
4 changes: 0 additions & 4 deletions homeassistant/components/august/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
"codeowners": ["@bdraco"],
"config_flow": true,
"dhcp": [
{
"hostname": "yale-connect-plus",
"macaddress": "00177A*"
},
{
"hostname": "connect",
"macaddress": "D86162*"
Expand Down
81 changes: 81 additions & 0 deletions homeassistant/components/yale/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Support for Yale devices."""

from __future__ import annotations

from pathlib import Path
from typing import cast

from aiohttp import ClientResponseError
from yalexs.const import Brand
from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr

from .const import DOMAIN, PLATFORMS
from .data import YaleData
from .gateway import YaleGateway
from .util import async_create_yale_clientsession

type YaleConfigEntry = ConfigEntry[YaleData]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up yale from a config entry."""
session = async_create_yale_clientsession(hass)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_yale(hass, entry, yale_gateway)
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
except (YaleApiError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def async_setup_yale(
hass: HomeAssistant, entry: YaleConfigEntry, yale_gateway: YaleGateway
) -> None:
"""Set up the yale component."""
config = cast(YaleXSConfig, entry.data)
await yale_gateway.async_setup({**config, CONF_BRAND: Brand.YALE_GLOBAL})
await yale_gateway.async_authenticate()
await yale_gateway.async_refresh_access_token_if_needed()
data = entry.runtime_data = YaleData(hass, yale_gateway)
entry.async_on_unload(
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, data.async_stop)
)
entry.async_on_unload(data.async_stop)
await data.async_setup()


async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: YaleConfigEntry, device_entry: dr.DeviceEntry
) -> bool:
"""Remove yale config entry from a device if its no longer present."""
return not any(
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
and config_entry.runtime_data.get_device(identifier[1])
)
15 changes: 15 additions & 0 deletions homeassistant/components/yale/application_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""application_credentials platform the yale integration."""

from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant

OAUTH2_AUTHORIZE = "https://oauth.aaecosystem.com/authorize"
OAUTH2_TOKEN = "https://oauth.aaecosystem.com/access_token"


async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server."""
return AuthorizationServer(
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)
189 changes: 189 additions & 0 deletions homeassistant/components/yale/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"""Support for Yale binary sensors."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import partial
import logging

from yalexs.activity import Activity, ActivityType
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail, LockDoorStatus
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
from yalexs.util import update_lock_detail_from_activity

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later

from . import YaleConfigEntry, YaleData
from .entity import YaleDescriptionEntity
from .util import (
retrieve_ding_activity,
retrieve_doorbell_motion_activity,
retrieve_online_state,
retrieve_time_based_activity,
)

_LOGGER = logging.getLogger(__name__)

TIME_TO_RECHECK_DETECTION = timedelta(
seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3
)


@dataclass(frozen=True, kw_only=True)
class YaleDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Yale binary_sensor entity."""

value_fn: Callable[[YaleData, DoorbellDetail | LockDetail], Activity | None]
is_time_based: bool


SENSOR_TYPE_DOOR = BinarySensorEntityDescription(
key="open",
device_class=BinarySensorDeviceClass.DOOR,
)

SENSOR_TYPES_VIDEO_DOORBELL = (
YaleDoorbellBinarySensorEntityDescription(
key="motion",
device_class=BinarySensorDeviceClass.MOTION,
value_fn=retrieve_doorbell_motion_activity,
is_time_based=True,
),
YaleDoorbellBinarySensorEntityDescription(
key="image capture",
translation_key="image_capture",
value_fn=partial(
retrieve_time_based_activity, {ActivityType.DOORBELL_IMAGE_CAPTURE}
),
is_time_based=True,
),
YaleDoorbellBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=retrieve_online_state,
is_time_based=False,
),
)


SENSOR_TYPES_DOORBELL: tuple[YaleDoorbellBinarySensorEntityDescription, ...] = (
YaleDoorbellBinarySensorEntityDescription(
key="ding",
translation_key="ding",
device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=retrieve_ding_activity,
is_time_based=True,
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Yale binary sensors."""
data = config_entry.runtime_data
entities: list[BinarySensorEntity] = []

for lock in data.locks:
detail = data.get_device_detail(lock.device_id)
if detail.doorsense:
entities.append(YaleDoorBinarySensor(data, lock, SENSOR_TYPE_DOOR))

if detail.doorbell:
entities.extend(
YaleDoorbellBinarySensor(data, lock, description)
for description in SENSOR_TYPES_DOORBELL
)

for doorbell in data.doorbells:
entities.extend(
YaleDoorbellBinarySensor(data, doorbell, description)
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
)

async_add_entities(entities)


class YaleDoorBinarySensor(YaleDescriptionEntity, BinarySensorEntity):
"""Representation of an Yale Door binary sensor."""

_attr_device_class = BinarySensorDeviceClass.DOOR
description: BinarySensorEntityDescription

@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor and update activity."""
if door_activity := self._get_latest({ActivityType.DOOR_OPERATION}):
update_lock_detail_from_activity(self._detail, door_activity)
if door_activity.was_pushed:
self._detail.set_online(True)

if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}):
update_lock_detail_from_activity(self._detail, bridge_activity)
self._attr_available = self._detail.bridge_is_online
self._attr_is_on = self._detail.door_state == LockDoorStatus.OPEN


class YaleDoorbellBinarySensor(YaleDescriptionEntity, BinarySensorEntity):
"""Representation of an Yale binary sensor."""

entity_description: YaleDoorbellBinarySensorEntityDescription
_check_for_off_update_listener: Callable[[], None] | None = None

@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor."""
self._cancel_any_pending_updates()
self._attr_is_on = bool(
self.entity_description.value_fn(self._data, self._detail)
)

if self.entity_description.is_time_based:
self._attr_available = retrieve_online_state(self._data, self._detail)
self._schedule_update_to_recheck_turn_off_sensor()
else:
self._attr_available = True

@callback
def _async_scheduled_update(self, now: datetime) -> None:
"""Timer callback for sensor update."""
self._check_for_off_update_listener = None
self._update_from_data()
if not self.is_on:
self.async_write_ha_state()

def _schedule_update_to_recheck_turn_off_sensor(self) -> None:
"""Schedule an update to recheck the sensor to see if it is ready to turn off."""
# If the sensor is already off there is nothing to do
if not self.is_on:
return
self._check_for_off_update_listener = async_call_later(
self.hass, TIME_TO_RECHECK_DETECTION, self._async_scheduled_update
)

def _cancel_any_pending_updates(self) -> None:
"""Cancel any updates to recheck a sensor to see if it is ready to turn off."""
if not self._check_for_off_update_listener:
return
_LOGGER.debug("%s: canceled pending update", self.entity_id)
self._check_for_off_update_listener()
self._check_for_off_update_listener = None

async def async_will_remove_from_hass(self) -> None:
"""When removing cancel any scheduled updates."""
self._cancel_any_pending_updates()
await super().async_will_remove_from_hass()
32 changes: 32 additions & 0 deletions homeassistant/components/yale/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Support for Yale buttons."""

from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import YaleConfigEntry
from .entity import YaleEntityMixin


async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Yale lock wake buttons."""
data = config_entry.runtime_data
async_add_entities(YaleWakeLockButton(data, lock, "wake") for lock in data.locks)


class YaleWakeLockButton(YaleEntityMixin, ButtonEntity):
"""Representation of an Yale lock wake button."""

_attr_translation_key = "wake"

async def async_press(self) -> None:
"""Wake the device."""
await self._data.async_status_async(self._device_id, self._hyper_bridge)

@callback
def _update_from_data(self) -> None:
"""Nothing to update as buttons are stateless."""
Loading

0 comments on commit 03ead27

Please sign in to comment.