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

Split august and yale integrations #124677

Merged
merged 16 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,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:
bdraco marked this conversation as resolved.
Show resolved Hide resolved
"""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])
)
bdraco marked this conversation as resolved.
Show resolved Hide resolved
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
)
bdraco marked this conversation as resolved.
Show resolved Hide resolved

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
Loading