Compare commits

..

2 Commits

Author SHA1 Message Date
Paulus Schoutsen
554c6e0cca Fix service helper tests 2025-05-13 20:17:07 +00:00
Paulus Schoutsen
7bac640267 Add area motion entity ID 2025-05-13 17:29:25 +00:00
548 changed files with 5963 additions and 28750 deletions

View File

@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

View File

@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Dependency review
uses: actions/dependency-review-action@v4.7.1
uses: actions/dependency-review-action@v4.7.0
with:
license-check: false # We use our own license audit checks
@@ -1320,7 +1320,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
flags: full-suite
@@ -1463,7 +1463,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -270,7 +270,6 @@ homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*

6
CODEOWNERS generated
View File

@@ -710,8 +710,6 @@ build.json @home-assistant/supervisor
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
@@ -1486,8 +1484,8 @@ build.json @home-assistant/supervisor
/tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii @jb101010-2
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen

View File

@@ -6,7 +6,6 @@ from typing import Any, Concatenate
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -30,7 +29,6 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
model_id=measures.model,
serial_number=coordinator.serial_number,
sw_version=measures.firmware_version,
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
)

View File

@@ -47,7 +47,7 @@ from .const import (
CONF_VIDEO_SOURCE,
DEFAULT_STREAM_PROFILE,
DEFAULT_VIDEO_SOURCE,
DOMAIN,
DOMAIN as AXIS_DOMAIN,
)
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
@@ -58,7 +58,7 @@ DEFAULT_PROTOCOL = "https"
PROTOCOL_CHOICES = ["https", "http"]
class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
"""Handle a Axis config flow."""
VERSION = 3
@@ -146,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
model = self.config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]

View File

@@ -24,7 +24,7 @@ from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
type BlueCurrentConfigEntry = ConfigEntry[Connector]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
PLATFORMS = [Platform.SENSOR]
CHARGE_POINTS = "CHARGE_POINTS"
DATA = "data"
DELAY = 5

View File

@@ -1,89 +0,0 @@
"""Support for Blue Current buttons."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from bluecurrent_api.client import Client
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BlueCurrentConfigEntry, Connector
from .entity import ChargepointEntity
@dataclass(kw_only=True, frozen=True)
class ChargePointButtonEntityDescription(ButtonEntityDescription):
"""Describes a Blue Current button entity."""
function: Callable[[Client, str], Coroutine[Any, Any, None]]
CHARGE_POINT_BUTTONS = (
ChargePointButtonEntityDescription(
key="reset",
translation_key="reset",
function=lambda client, evse_id: client.reset(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="reboot",
translation_key="reboot",
function=lambda client, evse_id: client.reboot(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="stop_charge_session",
translation_key="stop_charge_session",
function=lambda client, evse_id: client.stop_session(evse_id),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: BlueCurrentConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Blue Current buttons."""
connector: Connector = entry.runtime_data
async_add_entities(
ChargePointButton(
connector,
button,
evse_id,
)
for evse_id in connector.charge_points
for button in CHARGE_POINT_BUTTONS
)
class ChargePointButton(ChargepointEntity, ButtonEntity):
"""Define a charge point button."""
has_value = True
entity_description: ChargePointButtonEntityDescription
def __init__(
self,
connector: Connector,
description: ChargePointButtonEntityDescription,
evse_id: str,
) -> None:
"""Initialize the button."""
super().__init__(connector, evse_id)
self.entity_description = description
self._attr_unique_id = f"{description.key}_{evse_id}"
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.function(self.connector.client, self.evse_id)

View File

@@ -1,5 +1,7 @@
"""Entity representing a Blue Current charge point."""
from abc import abstractmethod
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -15,12 +17,12 @@ class BlueCurrentEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
has_value = False
def __init__(self, connector: Connector, signal: str) -> None:
"""Initialize the entity."""
self.connector = connector
self.signal = signal
self.has_value = False
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -41,6 +43,7 @@ class BlueCurrentEntity(Entity):
return self.connector.connected and self.has_value
@callback
@abstractmethod
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""

View File

@@ -19,17 +19,6 @@
"current_left": {
"default": "mdi:gauge"
}
},
"button": {
"reset": {
"default": "mdi:restart"
},
"reboot": {
"default": "mdi:restart-alert"
},
"stop_charge_session": {
"default": "mdi:stop"
}
}
}
}

View File

@@ -113,17 +113,6 @@
"grid_max_current": {
"name": "Max grid current"
}
},
"button": {
"stop_charge_session": {
"name": "Stop charge session"
},
"reboot": {
"name": "Reboot"
},
"reset": {
"name": "Reset"
}
}
}
}

View File

@@ -22,7 +22,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
from .const import (
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
DOMAIN as BMW_DOMAIN,
SCAN_INTERVALS,
)
_LOGGER = logging.getLogger(__name__)
@@ -57,7 +63,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}",
update_interval=timedelta(
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
),
@@ -75,26 +81,26 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
# Clear refresh token and trigger reauth if previous update failed as well
self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="invalid_auth",
) from err
except (MyBMWAPIError, RequestError) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err

View File

@@ -7,17 +7,15 @@ from ssl import SSLError
from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
PLATFORMS: list[Platform] = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
]
@@ -54,11 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
device_registry = dr.async_get(hass)
mac = entry.data.get(CONF_MAC)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",

View File

@@ -34,9 +34,6 @@ async def async_setup_entry(
)
PARALLEL_UPDATES = 0
class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel."""

View File

@@ -1,220 +0,0 @@
"""Support for Bosch Alarm Panel binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity, BoschAlarmEntity, BoschAlarmPointEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmFaultEntityDescription(BinarySensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
fault: int
FAULT_TYPES = [
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_low",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.BATTERY,
fault=ALARM_PANEL_FAULTS.BATTERY_LOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_mising",
translation_key="panel_fault_battery_mising",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.BATTERY_MISING,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_ac_fail",
translation_key="panel_fault_ac_fail",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.AC_FAIL,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_phone_line_failure",
translation_key="panel_fault_phone_line_failure",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
fault=ALARM_PANEL_FAULTS.PHONE_LINE_FAILURE,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_parameter_crc_fail_in_pif",
translation_key="panel_fault_parameter_crc_fail_in_pif",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.PARAMETER_CRC_FAIL_IN_PIF,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_communication_fail_since_rps_hang_up",
translation_key="panel_fault_communication_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.COMMUNICATION_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_sdi_fail_since_rps_hang_up",
translation_key="panel_fault_sdi_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.SDI_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_user_code_tamper_since_rps_hang_up",
translation_key="panel_fault_user_code_tamper_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.USER_CODE_TAMPER_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_fail_to_call_rps_since_rps_hang_up",
translation_key="panel_fault_fail_to_call_rps_since_rps_hang_up",
entity_registry_enabled_default=False,
fault=ALARM_PANEL_FAULTS.FAIL_TO_CALL_RPS_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_point_bus_fail_since_rps_hang_up",
translation_key="panel_fault_point_bus_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.POINT_BUS_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_overflow",
translation_key="panel_fault_log_overflow",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_OVERFLOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_threshold",
translation_key="panel_fault_log_threshold",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_THRESHOLD,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors for alarm points and the connection status."""
panel = config_entry.runtime_data
entities: list[BinarySensorEntity] = [
PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id)
for point_id in panel.points
]
entities.extend(
PanelFaultsSensor(
panel,
config_entry.unique_id or config_entry.entry_id,
fault_type,
)
for fault_type in FAULT_TYPES
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "away"
)
for area_id in panel.areas
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "home"
)
for area_id in panel.areas
)
async_add_entities(entities)
PARALLEL_UPDATES = 0
class PanelFaultsSensor(BoschAlarmEntity, BinarySensorEntity):
"""A binary sensor entity for each fault type in a bosch alarm panel."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: BoschAlarmFaultEntityDescription
def __init__(
self,
panel: Panel,
unique_id: str,
entity_description: BoschAlarmFaultEntityDescription,
) -> None:
"""Set up a binary sensor entity for each fault type in a bosch alarm panel."""
super().__init__(panel, unique_id, True)
self.entity_description = entity_description
self._fault_type = entity_description.fault
self._attr_unique_id = f"{unique_id}_fault_{entity_description.key}"
@property
def is_on(self) -> bool:
"""Return if this fault has occurred."""
return self._fault_type in self.panel.panel_faults_ids
class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity):
"""A binary sensor entity showing if a panel is ready to arm."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self, panel: Panel, area_id: int, unique_id: str, arm_type: str
) -> None:
"""Set up a binary sensor entity for the arming status in a bosch alarm panel."""
super().__init__(panel, area_id, unique_id, False, False, True)
self.panel = panel
self._arm_type = arm_type
self._attr_translation_key = f"area_ready_to_arm_{arm_type}"
self._attr_unique_id = f"{self._area_unique_id}_ready_to_arm_{arm_type}"
@property
def is_on(self) -> bool:
"""Return if this panel is ready to arm."""
if self._arm_type == "away":
return self._area.all_ready
if self._arm_type == "home":
return self._area.all_ready or self._area.part_ready
return False
class PointSensor(BoschAlarmPointEntity, BinarySensorEntity):
"""A binary sensor entity for a point in a bosch alarm panel."""
_attr_name = None
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a binary sensor entity for a point in a bosch alarm panel."""
super().__init__(panel, point_id, unique_id)
self._attr_unique_id = self._point_unique_id
@property
def is_on(self) -> bool:
"""Return if this point sensor is on."""
return self._point.is_open()

View File

@@ -6,13 +6,12 @@ import asyncio
from collections.abc import Mapping
import logging
import ssl
from typing import Any, Self
from typing import Any
from bosch_alarm_mode2 import Panel
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_DHCP,
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlow,
@@ -21,14 +20,11 @@ from homeassistant.config_entries import (
from homeassistant.const import (
CONF_CODE,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_PASSWORD,
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
@@ -92,12 +88,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Init config flow."""
self._data: dict[str, Any] = {}
self.mac: str | None = None
self.host: str | None = None
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return self.mac == other_flow.mac or self.host == other_flow.host
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -106,12 +96,9 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
if self.source == SOURCE_USER:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(user_input, 0)
(model, serial) = await try_connect(user_input, 0)
except (
OSError,
ConnectionRefusedError,
@@ -142,55 +129,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
self.mac = format_mac(discovery_info.macaddress)
self.host = discovery_info.ip
if self.hass.config_entries.flow.async_has_matching_flow(self):
return self.async_abort(reason="already_in_progress")
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data[CONF_MAC] == self.mac:
result = self.hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_HOST: discovery_info.ip,
},
)
if result:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(
{CONF_HOST: discovery_info.ip, CONF_PORT: 7700}, 0
)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
asyncio.exceptions.TimeoutError,
):
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
self.context["title_placeholders"] = {
"model": model,
"host": discovery_info.ip,
}
self._data = {
CONF_HOST: discovery_info.ip,
CONF_MAC: self.mac,
CONF_MODEL: model,
CONF_PORT: 7700,
}
return await self.async_step_auth()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -234,7 +172,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if serial_number:
await self.async_set_unique_id(str(serial_number))
if self.source in (SOURCE_USER, SOURCE_DHCP):
if self.source == SOURCE_USER:
if serial_number:
self._abort_if_unique_id_configured()
else:
@@ -246,7 +184,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
)
if serial_number:
self._abort_if_unique_id_mismatch(reason="device_mismatch")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data=self._data,

View File

@@ -17,13 +17,9 @@ class BoschAlarmEntity(Entity):
_attr_has_entity_name = True
def __init__(
self, panel: Panel, unique_id: str, observe_faults: bool = False
) -> None:
def __init__(self, panel: Panel, unique_id: str) -> None:
"""Set up a entity for a bosch alarm panel."""
self.panel = panel
self._observe_faults = observe_faults
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
@@ -38,14 +34,10 @@ class BoschAlarmEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
class BoschAlarmAreaEntity(BoschAlarmEntity):
@@ -96,33 +88,6 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
self._area.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmPointEntity(BoschAlarmEntity):
"""A base entity for point related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._point_id = point_id
self._point_unique_id = f"{unique_id}_point_{point_id}"
self._point = panel.points[point_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._point_unique_id)},
name=self._point.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._point.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._point.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmDoorEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""

View File

@@ -1,15 +1,6 @@
{
"entity": {
"sensor": {
"alarms_gas": {
"default": "mdi:alert-circle"
},
"alarms_fire": {
"default": "mdi:alert-circle"
},
"alarms_burglary": {
"default": "mdi:alert-circle"
},
"faulting_points": {
"default": "mdi:alert-circle"
}
@@ -33,44 +24,6 @@
"on": "mdi:lock-open"
}
}
},
"binary_sensor": {
"panel_fault_parameter_crc_fail_in_pif": {
"default": "mdi:alert-circle"
},
"panel_fault_phone_line_failure": {
"default": "mdi:alert-circle"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_log_overflow": {
"default": "mdi:alert-circle"
},
"panel_fault_log_threshold": {
"default": "mdi:alert-circle"
},
"area_ready_to_arm_away": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-lock"
}
},
"area_ready_to_arm_home": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-home"
}
}
}
}
}

View File

@@ -3,11 +3,6 @@
"name": "Bosch Alarm",
"codeowners": ["@mag1024", "@sanjay900"],
"config_flow": true,
"dhcp": [
{
"macaddress": "000463*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",

View File

@@ -39,15 +39,15 @@ rules:
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
parallel-updates: todo
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: done
discovery: done
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo

View File

@@ -6,7 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES
from bosch_alarm_mode2.panel import Area
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
@@ -16,53 +15,18 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity
ALARM_TYPES = {
"burglary": {
ALARM_MEMORY_PRIORITIES.BURGLARY_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.BURGLARY_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.BURGLARY_ALARM: "alarm",
},
"gas": {
ALARM_MEMORY_PRIORITIES.GAS_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.GAS_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.GAS_ALARM: "alarm",
},
"fire": {
ALARM_MEMORY_PRIORITIES.FIRE_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.FIRE_ALARM: "alarm",
},
}
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
value_fn: Callable[[Area], str | int]
value_fn: Callable[[Area], int]
observe_alarms: bool = False
observe_ready: bool = False
observe_status: bool = False
def priority_value_fn(priority_info: dict[int, str]) -> Callable[[Area], str]:
"""Build a value_fn for a given priority type."""
return lambda area: next(
(key for priority, key in priority_info.items() if priority in area.alarms_ids),
"no_issues",
)
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
*[
BoschAlarmSensorEntityDescription(
key=f"alarms_{key}",
translation_key=f"alarms_{key}",
value_fn=priority_value_fn(priority_type),
observe_alarms=True,
)
for key, priority_type in ALARM_TYPES.items()
],
BoschAlarmSensorEntityDescription(
key="faulting_points",
translation_key="faulting_points",
@@ -117,6 +81,6 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
@property
def native_value(self) -> str | int:
def native_value(self) -> int:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._area)

View File

@@ -1,6 +1,5 @@
{
"config": {
"flow_title": "{model} ({host})",
"step": {
"user": {
"data": {
@@ -43,7 +42,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
@@ -58,95 +56,22 @@
"message": "Incorrect credentials for panel."
},
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is momentarily unlocked."
"message": "Door cannot be manipulated while it is being cycled."
}
},
"entity": {
"binary_sensor": {
"panel_fault_battery_mising": {
"name": "Battery missing"
},
"panel_fault_ac_fail": {
"name": "AC Failure"
},
"panel_fault_parameter_crc_fail_in_pif": {
"name": "CRC failure in panel configuration"
},
"panel_fault_phone_line_failure": {
"name": "Phone line failure"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"name": "SDI failure since RPS hang up"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"name": "User code tamper since RPS hang up"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"name": "Failure to call RPS since RPS hang up"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"name": "Point bus failure since RPS hang up"
},
"panel_fault_log_overflow": {
"name": "Log overflow"
},
"panel_fault_log_threshold": {
"name": "Log threshold reached"
},
"area_ready_to_arm_away": {
"name": "Area ready to arm away",
"state": {
"on": "Ready",
"off": "Not ready"
}
},
"area_ready_to_arm_home": {
"name": "Area ready to arm home",
"state": {
"on": "Ready",
"off": "Not ready"
}
}
},
"switch": {
"secured": {
"name": "Secured"
},
"cycling": {
"name": "Momentarily unlocked"
"name": "Cycling"
},
"locked": {
"name": "Locked"
}
},
"sensor": {
"alarms_gas": {
"name": "Gas alarm issues",
"state": {
"supervisory": "Supervisory",
"trouble": "Trouble",
"alarm": "Alarm",
"no_issues": "No issues"
}
},
"alarms_fire": {
"name": "Fire alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"alarms_burglary": {
"name": "Burglary alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"faulting_points": {
"name": "Faulting points",
"unit_of_measurement": "points"

View File

@@ -60,7 +60,7 @@ from .const import (
ADDED_CAST_DEVICES_KEY,
CAST_MULTIZONE_MANAGER_KEY,
CONF_IGNORE_CEC,
DOMAIN,
DOMAIN as CAST_DOMAIN,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
@@ -315,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self._cast_view_remove_handler: CALLBACK_TYPE | None = None
self._attr_unique_id = str(cast_info.uuid)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(cast_info.uuid).replace("-", ""))},
identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))},
manufacturer=str(cast_info.cast_info.manufacturer),
model=cast_info.cast_info.model_name,
name=str(cast_info.friendly_name),
@@ -591,7 +591,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
"""Generate root node."""
children = []
# Add media browsers
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
@@ -650,7 +650,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
platform: CastProtocol
assert media_content_type is not None
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
@@ -680,7 +680,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
# Handle media supported by a known cast app
if media_type == DOMAIN:
if media_type == CAST_DOMAIN:
try:
app_data = json.loads(media_id)
if metadata := extra.get("metadata"):
@@ -712,7 +712,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return
# Try the cast platforms
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
result = await platform.async_play_media(
self.hass, self.entity_id, chromecast, media_type, media_id
)

View File

@@ -43,7 +43,7 @@ from homeassistant.util.dt import utcnow
from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DOMAIN,
DOMAIN as CLOUD_DOMAIN,
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_SHOULD_EXPOSE,
@@ -55,7 +55,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_ALEXA = f"{DOMAIN}.{ALEXA_DOMAIN}"
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
# Time to wait when entity preferences have changed before syncing it to
# the cloud.

View File

@@ -41,7 +41,7 @@ from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DEFAULT_DISABLE_2FA,
DOMAIN,
DOMAIN as CLOUD_DOMAIN,
PREF_DISABLE_2FA,
PREF_SHOULD_EXPOSE,
)
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_GOOGLE = f"{DOMAIN}.{GOOGLE_DOMAIN}"
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
SUPPORTED_DOMAINS = {

View File

@@ -134,9 +134,11 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
self._attr_current_temperature = values[0] / 10
self._attr_hvac_action = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_action = HVACAction.OFF
if not _active:
self._attr_hvac_action = HVACAction.IDLE
elif _mode in API_STATUS:
if _mode in API_STATUS:
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
self._attr_hvac_mode = None

View File

@@ -44,6 +44,7 @@ def websocket_list_areas(
vol.Optional("humidity_entity_id"): vol.Any(str, None),
vol.Optional("icon"): str,
vol.Optional("labels"): [str],
vol.Optional("motion_entity_id"): vol.Any(str, None),
vol.Required("name"): str,
vol.Optional("picture"): vol.Any(str, None),
vol.Optional("temperature_entity_id"): vol.Any(str, None),
@@ -112,6 +113,7 @@ def websocket_delete_area(
vol.Optional("humidity_entity_id"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
vol.Optional("labels"): [str],
vol.Optional("motion_entity_id"): vol.Any(str, None),
vol.Optional("name"): str,
vol.Optional("picture"): vol.Any(str, None),
vol.Optional("temperature_entity_id"): vol.Any(str, None),

View File

@@ -17,7 +17,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import CONF_MASTER_GATEWAY, DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS
from ..const import (
CONF_MASTER_GATEWAY,
DOMAIN as DECONZ_DOMAIN,
HASSIO_CONFIGURATION_URL,
PLATFORMS,
)
from .config import DeconzConfig
if TYPE_CHECKING:
@@ -188,7 +193,7 @@ class DeconzHub:
config_entry_id=self.config_entry.entry_id,
configuration_url=configuration_url,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self.api.config.bridge_id)},
identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)},
manufacturer="Dresden Elektronik",
model=self.api.config.model_id,
name=self.api.config.name,

View File

@@ -6,16 +6,12 @@ from datetime import datetime
from typing import Any
from homeassistant.components.media_player import (
BrowseMedia,
MediaClass,
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
RepeatMode,
SearchMedia,
SearchMediaQuery,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -411,18 +407,3 @@ class DemoSearchPlayer(AbstractDemoPlayer):
"""A Demo media player that supports searching."""
_attr_supported_features = SEARCH_PLAYER_SUPPORT
async def async_search_media(self, query: SearchMediaQuery) -> SearchMedia:
"""Demo implementation of search media."""
return SearchMedia(
result=[
BrowseMedia(
title="Search result",
media_class=MediaClass.MOVIE,
media_content_type=MediaType.MOVIE,
media_content_id="search_result_id",
can_play=True,
can_expand=False,
)
]
)

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.1.1"],
"requirements": ["denonavr==1.1.0"],
"ssdp": [
{
"manufacturer": "Denon",

View File

@@ -8,7 +8,11 @@ import voluptuous as vol
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
TriggerProtocol,
)
from homeassistant.helpers.typing import ConfigType
from . import (
@@ -21,28 +25,13 @@ from .helpers import async_validate_device_automation_config
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
class DeviceAutomationTriggerProtocol(Protocol):
class DeviceAutomationTriggerProtocol(TriggerProtocol, Protocol):
"""Define the format of device_trigger modules.
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config.
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config
from TriggerProtocol.
"""
TRIGGER_SCHEMA: vol.Schema
async def async_validate_trigger_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
async def async_attach_trigger(
self,
hass: HomeAssistant,
config: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
async def async_get_trigger_capabilities(
self, hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:

View File

@@ -7,7 +7,6 @@ from collections.abc import Callable
from datetime import timedelta
from fnmatch import translate
from functools import lru_cache, partial
from ipaddress import IPv4Address
import itertools
import logging
import re
@@ -23,7 +22,6 @@ from aiodiscover.discovery import (
from cached_ipaddress import cached_ip_addresses
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components.device_tracker import (
ATTR_HOST_NAME,
ATTR_IP,
@@ -423,33 +421,9 @@ class DHCPWatcher(WatcherBase):
response.ip_address, response.hostname, response.mac_address
)
async def async_get_adapter_indexes(self) -> list[int] | None:
"""Get the adapter indexes."""
adapters = await network.async_get_adapters(self.hass)
if network.async_only_default_interface_enabled(adapters):
return None
return [
adapter["index"]
for adapter in adapters
if (
adapter["enabled"]
and adapter["index"] is not None
and adapter["ipv4"]
and (
addresses := [IPv4Address(ip["address"]) for ip in adapter["ipv4"]]
)
and any(
ip for ip in addresses if not ip.is_loopback and not ip.is_global
)
)
]
async def async_start(self) -> None:
"""Start watching for dhcp packets."""
self._unsub = await aiodhcpwatcher.async_start(
self._async_process_dhcp_request,
await self.async_get_adapter_indexes(),
)
self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request)
class RediscoveryWatcher(WatcherBase):

View File

@@ -2,7 +2,6 @@
"domain": "dhcp",
"name": "DHCP Discovery",
"codeowners": ["@bdraco"],
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/dhcp",
"integration_type": "system",
"iot_class": "local_push",

View File

@@ -148,6 +148,11 @@ class EcoNetThermostat(EcoNetEntity[Thermostat], ClimateEntity):
if target_temp_low or target_temp_high:
self._econet.set_set_point(None, target_temp_high, target_temp_low)
@property
def is_aux_heat(self) -> bool:
"""Return true if aux heater."""
return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool, mode.

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
}

View File

@@ -6,8 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
from deebot_client.events import (
BatteryEvent,
ErrorEvent,
@@ -35,7 +34,7 @@ from homeassistant.const import (
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -60,15 +59,6 @@ class EcovacsSensorEntityDescription(
"""Ecovacs sensor entity description."""
value_fn: Callable[[EventT], StateType]
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
@callback
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
"""Get the area native unit of measurement based on device type."""
if device_type is DeviceType.MOWER:
return UnitOfArea.SQUARE_CENTIMETERS
return UnitOfArea.SQUARE_METERS
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
@@ -78,9 +68,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
capability_fn=lambda caps: caps.stats.clean,
value_fn=lambda e: e.area,
translation_key="stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
@@ -97,10 +85,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
value_fn=lambda e: e.area,
key="total_stats_area",
translation_key="total_stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
@@ -263,27 +249,6 @@ class EcovacsSensor(
entity_description: EcovacsSensorEntityDescription
def __init__(
self,
device: Device,
capability: CapabilityEvent,
entity_description: EcovacsSensorEntityDescription,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description, **kwargs)
if (
entity_description.native_unit_of_measurement_fn
and (
native_unit_of_measurement
:= entity_description.native_unit_of_measurement_fn(
device.capabilities.device_type
)
)
is not None
):
self._attr_native_unit_of_measurement = native_unit_of_measurement
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()

View File

@@ -13,7 +13,6 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,

View File

@@ -15,12 +15,6 @@
},
"night_temperature_offset": {
"default": "mdi:thermometer"
},
"system_led": {
"default": "mdi:led-on",
"state": {
"0": "mdi:led-off"
}
}
},
"sensor": {

View File

@@ -109,20 +109,6 @@ HEATER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalHeater], ..
),
)
GENERAL_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalDevice], ...] = (
EheimDigitalNumberDescription[EheimDigitalDevice](
key="system_led",
translation_key="system_led",
entity_category=EntityCategory.CONFIG,
native_min_value=0,
native_max_value=100,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.sys_led,
set_value_fn=lambda device, value: device.set_sys_led(int(value)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -152,10 +138,6 @@ async def async_setup_entry(
)
for description in HEATER_DESCRIPTIONS
)
entities.extend(
EheimDigitalNumber[EheimDigitalDevice](coordinator, device, description)
for description in GENERAL_DESCRIPTIONS
)
async_add_entities(entities)

View File

@@ -1,102 +0,0 @@
"""EHEIM Digital select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
"""Class describing EHEIM Digital select entities."""
value_fn: Callable[[_DeviceT_co], str | None]
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSelectDescription[EheimDigitalClassicVario], ...
] = (
EheimDigitalSelectDescription[EheimDigitalClassicVario](
key="filter_mode",
translation_key="filter_mode",
value_fn=(
lambda device: device.filter_mode.name.lower()
if device.filter_mode is not None
else None
),
set_value_fn=(
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
),
options=[name.lower() for name in FilterMode.__members__],
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: EheimDigitalConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the callbacks for the coordinator so select entities can be added as devices are found."""
coordinator = entry.runtime_data
def async_setup_device_entities(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the number entities for one or multiple devices."""
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
EheimDigitalSelect[EheimDigitalClassicVario](
coordinator, device, description
)
for description in CLASSICVARIO_DESCRIPTIONS
)
async_add_entities(entities)
coordinator.add_platform_callback(async_setup_device_entities)
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalSelect(
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
):
"""Represent an EHEIM Digital select entity."""
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalSelectDescription[_DeviceT_co],
) -> None:
"""Initialize an EHEIM Digital select entity."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{self._device_address}_{description.key}"
@override
async def async_select_option(self, option: str) -> None:
return await self.entity_description.set_value_fn(self._device, option)
@override
def _async_update_attrs(self) -> None:
self._attr_current_option = self.entity_description.value_fn(self._device)

View File

@@ -62,19 +62,6 @@
},
"night_temperature_offset": {
"name": "Night temperature offset"
},
"system_led": {
"name": "System LED brightness"
}
},
"select": {
"filter_mode": {
"name": "Filter mode",
"state": {
"manual": "Manual",
"pulse": "Pulse",
"bio": "Bio"
}
}
},
"sensor": {

View File

@@ -20,8 +20,10 @@ from homeassistant.components.climate import (
from homeassistant.const import PRECISION_WHOLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from . import ElkM1ConfigEntry
from .const import DOMAIN
from .entity import ElkEntity, create_elk_entities
SUPPORT_HVAC = [
@@ -76,6 +78,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
_attr_precision = PRECISION_WHOLE
_attr_supported_features = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.AUX_HEAT
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
@@ -125,6 +128,11 @@ class ElkThermostat(ElkEntity, ClimateEntity):
"""Return the current humidity."""
return self._element.humidity
@property
def is_aux_heat(self) -> bool:
"""Return if aux heater is on."""
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
@property
def fan_mode(self) -> str | None:
"""Return the fan setting."""
@@ -143,6 +151,34 @@ class ElkThermostat(ElkEntity, ClimateEntity):
thermostat_mode, fan_mode = HASS_TO_ELK_HVAC_MODES[hvac_mode]
self._elk_set(thermostat_mode, fan_mode)
async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
async_create_issue(
self.hass,
DOMAIN,
"migrate_aux_heat",
breaks_in_ha_version="2025.4.0",
is_fixable=True,
is_persistent=True,
translation_key="migrate_aux_heat",
severity=IssueSeverity.WARNING,
)
self._elk_set(ThermostatMode.EMERGENCY_HEAT, None)
async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
async_create_issue(
self.hass,
DOMAIN,
"migrate_aux_heat",
breaks_in_ha_version="2025.4.0",
is_fixable=True,
is_persistent=True,
translation_key="migrate_aux_heat",
severity=IssueSeverity.WARNING,
)
self._elk_set(ThermostatMode.HEAT, None)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
thermostat_mode, elk_fan_mode = HASS_TO_ELK_FAN_MODES[fan_mode]

View File

@@ -189,5 +189,18 @@
"name": "Sensor zone trigger",
"description": "Triggers zone."
}
},
"issues": {
"migrate_aux_heat": {
"title": "Migration of Elk-M1 set_aux_heat action",
"fix_flow": {
"step": {
"confirm": {
"description": "The Elk-M1 `set_aux_heat` action has been migrated. A new emergency heat switch entity is available for each thermostat.\n\nUpdate any automations to use the new emergency heat switch entity. When this is done, select **Submit** to fix this issue.",
"title": "[%key:component::elkm1::issues::migrate_aux_heat::title%]"
}
}
}
}
}
}

View File

@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.8"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@@ -239,6 +239,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
assert entry_data.device_info is not None
device_info = entry_data.device_info
self._device_info = device_info
self._on_entry_data_changed()
self._key = entity_info.key
self._state_type = state_type
@@ -326,11 +327,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
@callback
def _on_entry_data_changed(self) -> None:
entry_data = self._entry_data
# Update the device info since it can change
# when the device is reconnected
if TYPE_CHECKING:
assert entry_data.device_info is not None
self._device_info = entry_data.device_info
self._api_version = entry_data.api_version
self._client = entry_data.client
if self._device_info.has_deep_sleep:

View File

@@ -63,7 +63,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
if self._supports_speed_levels:
data["speed_level"] = math.ceil(
percentage_to_ranged_value(
(1, self._static_info.supported_speed_count), percentage
(1, self._static_info.supported_speed_levels), percentage
)
)
else:
@@ -121,7 +121,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
)
return ranged_value_to_percentage(
(1, self._static_info.supported_speed_count), self._state.speed_level
(1, self._static_info.supported_speed_levels), self._state.speed_level
)
@property
@@ -164,7 +164,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
if not supports_speed_levels:
self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
else:
self._attr_speed_count = static_info.supported_speed_count
self._attr_speed_count = static_info.supported_speed_levels
async_setup_entry = partial(

View File

@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, cast
from aioesphomeapi import (
APIVersion,
ColorMode as ESPHomeColorMode,
EntityInfo,
LightColorCapability,
LightInfo,
@@ -107,15 +106,15 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
@lru_cache
def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
def _color_mode_to_ha(mode: int) -> str:
"""Convert an esphome color mode to a HA color mode constant.
Choose the color mode that best matches the feature-set.
"""
candidates: list[tuple[ColorMode, LightColorCapability]] = []
candidates = []
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
for caps in cap_lists:
if caps.value == mode:
if caps == mode:
# exact match
return ha_mode
if (mode & caps) == caps:
@@ -132,8 +131,8 @@ def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
@lru_cache
def _filter_color_modes(
supported: list[ESPHomeColorMode], features: LightColorCapability
) -> tuple[ESPHomeColorMode, ...]:
supported: list[int], features: LightColorCapability
) -> tuple[int, ...]:
"""Filter the given supported color modes.
Excluding all values that don't have the requested features.
@@ -157,7 +156,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
"""A light implementation for ESPHome."""
_native_supported_color_modes: tuple[ESPHomeColorMode, ...]
_native_supported_color_modes: tuple[int, ...]
_supports_color_mode = False
@property

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==31.0.1",
"aioesphomeapi==30.2.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.15.1"
],

View File

@@ -88,9 +88,9 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
return
if (
state_class == EsphomeSensorStateClass.MEASUREMENT
and static_info.legacy_last_reset_type == LastResetType.AUTO
and static_info.last_reset_type == LastResetType.AUTO
):
# Legacy, legacy_last_reset_type auto was the equivalent to the
# Legacy, last_reset_type auto was the equivalent to the
# TOTAL_INCREASING state class
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
else:

View File

@@ -35,7 +35,7 @@ async def validate_host(
hass: HomeAssistant, host: str
) -> tuple[str, FroniusConfigEntryData]:
"""Validate the user input allows us to connect."""
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
fronius = Fronius(async_get_clientsession(hass), host)
try:
datalogger_info: dict[str, Any]

View File

@@ -4,13 +4,13 @@
"current_dc": {
"default": "mdi:current-dc"
},
"current_dc_mppt_no": {
"current_dc_2": {
"default": "mdi:current-dc"
},
"voltage_dc": {
"default": "mdi:current-dc"
},
"voltage_dc_mppt_no": {
"voltage_dc_2": {
"default": "mdi:current-dc"
},
"co2_factor": {

View File

@@ -168,26 +168,6 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "2"},
),
FroniusSensorEntityDescription(
key="current_dc_3",
default_value=0,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "3"},
),
FroniusSensorEntityDescription(
key="current_dc_4",
default_value=0,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "4"},
),
FroniusSensorEntityDescription(
key="power_ac",
@@ -217,26 +197,6 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "2"},
),
FroniusSensorEntityDescription(
key="voltage_dc_3",
default_value=0,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "3"},
),
FroniusSensorEntityDescription(
key="voltage_dc_4",
default_value=0,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "4"},
),
# device status entities
FroniusSensorEntityDescription(
@@ -767,7 +727,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
self.response_key = description.response_key or description.key
self.solar_net_id = solar_net_id
self._attr_native_value = self._get_entity_value()
self._attr_translation_key = description.translation_key or description.key
self._attr_translation_key = description.key
def _device_data(self) -> dict[str, Any]:
"""Extract information for SolarNet device from coordinator data."""

View File

@@ -52,8 +52,8 @@
"current_dc": {
"name": "DC current"
},
"current_dc_mppt_no": {
"name": "DC current {mppt_no}"
"current_dc_2": {
"name": "DC current 2"
},
"power_ac": {
"name": "AC power"
@@ -64,8 +64,8 @@
"voltage_dc": {
"name": "DC voltage"
},
"voltage_dc_mppt_no": {
"name": "DC voltage {mppt_no}"
"voltage_dc_2": {
"name": "DC voltage 2"
},
"inverter_state": {
"name": "Inverter state"

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250516.0"]
"requirements": ["home-assistant-frontend==20250509.0"]
}

View File

@@ -20,12 +20,9 @@ from homeassistant.helpers import config_entry_flow, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
type GeofencyConfigEntry = ConfigEntry[set[str]]
PLATFORMS = [Platform.DEVICE_TRACKER]
CONF_MOBILE_BEACONS = "mobile_beacons"
@@ -78,13 +75,16 @@ WEBHOOK_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_DATA_GEOFENCY: HassKey[list[str]] = HassKey(DOMAIN)
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Geofency component."""
mobile_beacons = hass_config.get(DOMAIN, {}).get(CONF_MOBILE_BEACONS, [])
hass.data[_DATA_GEOFENCY] = [slugify(beacon) for beacon in mobile_beacons]
config = hass_config.get(DOMAIN, {})
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
hass.data[DOMAIN] = {
"beacons": [slugify(beacon) for beacon in mobile_beacons],
"devices": set(),
"unsub_device_tracker": {},
}
return True
@@ -99,7 +99,7 @@ async def handle_webhook(
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
)
if _is_mobile_beacon(data, hass.data[_DATA_GEOFENCY]):
if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]):
return _set_location(hass, data, None)
if data["entry"] == LOCATION_ENTRY:
location_name = data["name"]
@@ -140,9 +140,8 @@ def _set_location(hass, data, location_name):
return web.Response(text=f"Setting location for {device}")
async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure based on config entry."""
entry.runtime_data = set()
webhook.async_register(
hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@@ -151,9 +150,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) ->
return True
async def async_unload_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,6 +1,7 @@
"""Support for the Geofency device tracker platform."""
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -9,13 +10,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import TRACKER_UPDATE, GeofencyConfigEntry
from .const import DOMAIN
from . import DOMAIN, TRACKER_UPDATE
async def async_setup_entry(
hass: HomeAssistant,
config_entry: GeofencyConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Geofency config entry."""
@@ -23,16 +23,14 @@ async def async_setup_entry(
@callback
def _receive_data(device, gps, location_name, attributes):
"""Fire HA event to set location."""
if device in config_entry.runtime_data:
if device in hass.data[DOMAIN]["devices"]:
return
config_entry.runtime_data.add(device)
hass.data[DOMAIN]["devices"].add(device)
async_add_entities(
[GeofencyEntity(config_entry, device, gps, location_name, attributes)]
)
async_add_entities([GeofencyEntity(device, gps, location_name, attributes)])
config_entry.async_on_unload(
hass.data[DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = (
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
)
@@ -47,8 +45,8 @@ async def async_setup_entry(
}
if dev_ids:
config_entry.runtime_data.update(dev_ids)
async_add_entities(GeofencyEntity(config_entry, dev_id) for dev_id in dev_ids)
hass.data[DOMAIN]["devices"].update(dev_ids)
async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids)
class GeofencyEntity(TrackerEntity, RestoreEntity):
@@ -57,9 +55,8 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
_attr_has_entity_name = True
_attr_name = None
def __init__(self, entry, device, gps=None, location_name=None, attributes=None):
def __init__(self, device, gps=None, location_name=None, attributes=None):
"""Set up Geofency entity."""
self._entry = entry
self._attr_extra_state_attributes = attributes or {}
self._name = device
self._attr_location_name = location_name
@@ -96,7 +93,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
"""Clean up after entity before removal."""
await super().async_will_remove_from_hass()
self._unsub_dispatcher()
self._entry.runtime_data.remove(self.unique_id)
self.hass.data[DOMAIN]["devices"].remove(self.unique_id)
@callback
def _async_receive_data(self, device, gps, location_name, attributes):

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
"iot_class": "cloud_polling",
"loggers": ["google", "homeassistant.helpers.location"],
"requirements": ["google-maps-routing==0.6.15"]
"requirements": ["google-maps-routing==0.6.14"]
}

View File

@@ -24,8 +24,6 @@ from .const import (
DOMAIN,
)
type GPSLoggerConfigEntry = ConfigEntry[set[str]]
PLATFORMS = [Platform.DEVICE_TRACKER]
TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
@@ -90,9 +88,9 @@ async def handle_webhook(
return web.Response(text=f"Setting location for {device}")
async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure based on config entry."""
entry.runtime_data = set()
hass.data.setdefault(DOMAIN, {"devices": set(), "unsub_device_tracker": {}})
webhook.async_register(
hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@@ -105,6 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,6 +1,7 @@
"""Support for the GPSLogger device tracking."""
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY,
@@ -14,20 +15,19 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import TRACKER_UPDATE, GPSLoggerConfigEntry
from . import DOMAIN, TRACKER_UPDATE
from .const import (
ATTR_ACTIVITY,
ATTR_ALTITUDE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
DOMAIN,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: GPSLoggerConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Configure a dispatcher connection based on a config entry."""
@@ -35,14 +35,16 @@ async def async_setup_entry(
@callback
def _receive_data(device, gps, battery, accuracy, attrs):
"""Receive set location."""
if device in entry.runtime_data:
if device in hass.data[DOMAIN]["devices"]:
return
entry.runtime_data.add(device)
hass.data[DOMAIN]["devices"].add(device)
async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)])
entry.async_on_unload(async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data))
hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = (
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
)
# Restore previously loaded devices
dev_reg = dr.async_get(hass)
@@ -56,7 +58,7 @@ async def async_setup_entry(
entities = []
for dev_id in dev_ids:
entry.runtime_data.add(dev_id)
hass.data[DOMAIN]["devices"].add(dev_id)
entity = GPSLoggerEntity(dev_id, None, None, None, None)
entities.append(entity)

View File

@@ -1,29 +1,33 @@
"""The Gree Climate integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_time_interval
from .const import DISCOVERY_SCAN_INTERVAL
from .coordinator import DiscoveryService, GreeConfigEntry, GreeRuntimeData
from .const import (
COORDINATORS,
DATA_DISCOVERY_SERVICE,
DISCOVERY_SCAN_INTERVAL,
DISPATCHERS,
DOMAIN,
)
from .coordinator import DiscoveryService
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Gree Climate from a config entry."""
hass.data.setdefault(DOMAIN, {})
gree_discovery = DiscoveryService(hass, entry)
entry.runtime_data = GreeRuntimeData(
discovery_service=gree_discovery, coordinators=[]
)
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
async def _async_scan_update(_=None):
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
@@ -43,6 +47,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool
return True
async def async_unload_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
hass.data.pop(DATA_DISCOVERY_SERVICE)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(COORDINATORS, None)
hass.data[DOMAIN].pop(DISPATCHERS, None)
return unload_ok

View File

@@ -36,18 +36,21 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
COORDINATORS,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW,
TARGET_TEMPERATURE_STEP,
)
from .coordinator import DeviceDataUpdateCoordinator, GreeConfigEntry
from .coordinator import DeviceDataUpdateCoordinator
from .entity import GreeEntity
_LOGGER = logging.getLogger(__name__)
@@ -84,17 +87,17 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
async def async_setup_entry(
hass: HomeAssistant,
entry: GreeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@callback
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
def init_device(coordinator):
"""Register the device."""
async_add_entities([GreeClimateEntity(coordinator)])
for coordinator in entry.runtime_data.coordinators:
for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator)
entry.async_on_unload(

View File

@@ -1,10 +1,16 @@
"""Constants for the Gree Climate integration."""
COORDINATORS = "coordinators"
DATA_DISCOVERY_SERVICE = "gree_discovery"
DISCOVERY_SCAN_INTERVAL = 300
DISCOVERY_TIMEOUT = 8
DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered"
DISPATCHERS = "dispatchers"
DOMAIN = "gree"
COORDINATOR = "coordinator"
FAN_MEDIUM_LOW = "medium low"
FAN_MEDIUM_HIGH = "medium high"

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any
@@ -21,6 +20,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from homeassistant.util.dt import utcnow
from .const import (
COORDINATORS,
DISCOVERY_TIMEOUT,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
@@ -31,24 +31,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
type GreeConfigEntry = ConfigEntry[GreeRuntimeData]
@dataclass
class GreeRuntimeData:
"""RUntime data for Gree Climate integration."""
discovery_service: DiscoveryService
coordinators: list[DeviceDataUpdateCoordinator]
class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages polling for state changes from the device."""
config_entry: GreeConfigEntry
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: GreeConfigEntry, device: Device
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
) -> None:
"""Initialize the data update coordinator."""
super().__init__(
@@ -138,7 +128,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
class DiscoveryService(Listener):
"""Discovery event handler for gree devices."""
def __init__(self, hass: HomeAssistant, entry: GreeConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize discovery service."""
super().__init__()
self.hass = hass
@@ -147,6 +137,8 @@ class DiscoveryService(Listener):
self.discovery = Discovery(DISCOVERY_TIMEOUT)
self.discovery.add_listener(self)
hass.data[DOMAIN].setdefault(COORDINATORS, [])
async def device_found(self, device_info: DeviceInfo) -> None:
"""Handle new device found on the network."""
@@ -165,14 +157,14 @@ class DiscoveryService(Listener):
device.device_info.port,
)
coordo = DeviceDataUpdateCoordinator(self.hass, self.entry, device)
self.entry.runtime_data.coordinators.append(coordo)
self.hass.data[DOMAIN][COORDINATORS].append(coordo)
await coordo.async_refresh()
async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo)
async def device_update(self, device_info: DeviceInfo) -> None:
"""Handle updates in device information, update if ip has changed."""
for coordinator in self.entry.runtime_data.coordinators:
for coordinator in self.hass.data[DOMAIN][COORDINATORS]:
if coordinator.device.device_info.mac == device_info.mac:
coordinator.device.device_info.ip = device_info.ip
await coordinator.async_refresh()

View File

@@ -13,13 +13,13 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DISPATCH_DEVICE_DISCOVERED
from .coordinator import GreeConfigEntry
from .entity import DeviceDataUpdateCoordinator, GreeEntity
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
from .entity import GreeEntity
@dataclass(kw_only=True, frozen=True)
@@ -92,13 +92,13 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: GreeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@callback
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
def init_device(coordinator):
"""Register the device."""
async_add_entities(
@@ -106,7 +106,7 @@ async def async_setup_entry(
for description in GREE_SWITCHES
)
for coordinator in entry.runtime_data.coordinators:
for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator)
entry.async_on_unload(

View File

@@ -1,14 +1,5 @@
"""Shared constants for the greeneye_monitor integration."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from greeneye import Monitors
CONF_CHANNELS = "channels"
CONF_COUNTED_QUANTITY = "counted_quantity"
CONF_COUNTED_QUANTITY_PER_PULSE = "counted_quantity_per_pulse"
@@ -22,8 +13,8 @@ CONF_TEMPERATURE_SENSORS = "temperature_sensors"
CONF_TIME_UNIT = "time_unit"
CONF_VOLTAGE_SENSORS = "voltage"
DATA_GREENEYE_MONITOR = "greeneye_monitor"
DOMAIN = "greeneye_monitor"
DATA_GREENEYE_MONITOR: HassKey[Monitors] = HassKey(DOMAIN)
SENSOR_TYPE_CURRENT = "current_sensor"
SENSOR_TYPE_PULSE_COUNTER = "pulse_counter"

View File

@@ -109,7 +109,7 @@ async def async_setup_platform(
if len(monitor_configs) == 0:
monitors.remove_listener(on_new_monitor)
monitors = hass.data[DATA_GREENEYE_MONITOR]
monitors: greeneye.Monitors = hass.data[DATA_GREENEYE_MONITOR]
monitors.add_listener(on_new_monitor)
for monitor in monitors.monitors.values():
on_new_monitor(monitor)

View File

@@ -52,10 +52,10 @@ type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
"""Habitica Data Update Coordinator."""
config_entry: HabiticaConfigEntry
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: HabiticaConfigEntry, habitica: Habitica
self, hass: HomeAssistant, config_entry: ConfigEntry, habitica: Habitica
) -> None:
"""Initialize the Habitica data coordinator."""
super().__init__(

View File

@@ -234,7 +234,7 @@
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
"dishcare_dishwasher_program_auto_1": "Auto 1",
"dishcare_dishwasher_program_auto_2": "Auto 2",
"dishcare_dishwasher_program_auto_3": "Auto 3",
@@ -252,7 +252,7 @@
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
"dishcare_dishwasher_program_magic_daily": "Magic daily",
"dishcare_dishwasher_program_super_60": "Super 60ºC",
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
"dishcare_dishwasher_program_machine_care": "Machine care",
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",

View File

@@ -90,17 +90,16 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
minor_version=2,
)
if config_entry.minor_version <= 3:
# Add a `firmware_version` key if it doesn't exist to handle entries created
# with minor version 1.3 where the firmware version was not set.
if config_entry.minor_version == 2:
# Add a `firmware_version` key
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION),
FIRMWARE_VERSION: None,
},
version=1,
minor_version=4,
minor_version=3,
)
_LOGGER.debug(

View File

@@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Yellow."""
VERSION = 1
MINOR_VERSION = 4
MINOR_VERSION = 3
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate config flow."""
@@ -116,11 +116,6 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
if self._probed_firmware_info is not None
else ApplicationType.EZSP
).value,
FIRMWARE_VERSION: (
self._probed_firmware_info.firmware_version
if self._probed_firmware_info is not None
else None
),
},
)

View File

@@ -15,7 +15,6 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,

View File

@@ -1,138 +0,0 @@
"""The Homee alarm control panel platform."""
from dataclasses import dataclass
from pyHomee.const import AttributeChangedBy, AttributeType
from pyHomee.model import HomeeAttribute
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, HomeeConfigEntry
from .entity import HomeeEntity
from .helpers import get_name_for_enum
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class HomeeAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription):
"""A class that describes Homee alarm control panel entities."""
code_arm_required: bool = False
state_list: list[AlarmControlPanelState]
ALARM_DESCRIPTIONS = {
AttributeType.HOMEE_MODE: HomeeAlarmControlPanelEntityDescription(
key="homee_mode",
code_arm_required=False,
state_list=[
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_VACATION,
],
)
}
def get_supported_features(
state_list: list[AlarmControlPanelState],
) -> AlarmControlPanelEntityFeature:
"""Return supported features based on the state list."""
supported_features = AlarmControlPanelEntityFeature(0)
if AlarmControlPanelState.ARMED_HOME in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
if AlarmControlPanelState.ARMED_AWAY in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
if AlarmControlPanelState.ARMED_NIGHT in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
if AlarmControlPanelState.ARMED_VACATION in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_VACATION
return supported_features
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the alarm control panel component."""
async_add_entities(
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
)
class HomeeAlarmPanel(HomeeEntity, AlarmControlPanelEntity):
"""Representation of a Homee alarm control panel."""
entity_description: HomeeAlarmControlPanelEntityDescription
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeAlarmControlPanelEntityDescription,
) -> None:
"""Initialize a Homee alarm control panel entity."""
super().__init__(attribute, entry)
self.entity_description = description
self._attr_code_arm_required = description.code_arm_required
self._attr_supported_features = get_supported_features(description.state_list)
self._attr_translation_key = description.key
@property
def alarm_state(self) -> AlarmControlPanelState:
"""Return current state."""
return self.entity_description.state_list[int(self._attribute.current_value)]
@property
def changed_by(self) -> str:
"""Return by whom or what the entity was last changed."""
changed_by_name = get_name_for_enum(
AttributeChangedBy, self._attribute.changed_by
)
return f"{changed_by_name} - {self._attribute.changed_by_id}"
async def _async_set_alarm_state(self, state: AlarmControlPanelState) -> None:
"""Set the alarm state."""
if state in self.entity_description.state_list:
await self.async_set_homee_value(
self.entity_description.state_list.index(state)
)
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
# Since disarm is always present in the UI, we raise an error.
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="disarm_not_supported",
)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_HOME)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_NIGHT)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_AWAY)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_VACATION)

View File

@@ -27,20 +27,14 @@ class HomeeEntity(Entity):
)
self._entry = entry
node = entry.runtime_data.get_node_by_id(attribute.node_id)
# Homee hub itself has node-id -1
if node.id == -1:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
)
else:
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._host_connected = entry.runtime_data.connected

View File

@@ -26,11 +26,6 @@
}
},
"entity": {
"alarm_control_panel": {
"homee_mode": {
"name": "Status"
}
},
"binary_sensor": {
"blackout_alarm": {
"name": "Blackout"
@@ -375,9 +370,6 @@
"connection_closed": {
"message": "Could not connect to homee while setting attribute."
},
"disarm_not_supported": {
"message": "Disarm is not supported by homee."
},
"invalid_preset_mode": {
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
}

View File

@@ -3,6 +3,7 @@
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
@@ -20,7 +21,7 @@ from .const import (
HMIPC_HAPID,
HMIPC_NAME,
)
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
from .services import async_setup_services, async_unload_services
CONFIG_SCHEMA = vol.Schema(
@@ -44,6 +45,8 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the HomematicIP Cloud component."""
hass.data[DOMAIN] = {}
accesspoints = config.get(DOMAIN, [])
for conf in accesspoints:
@@ -66,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up an access point from a config entry."""
# 0.104 introduced config entry unique id, this makes upgrading possible
@@ -78,8 +81,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
)
hap = HomematicipHAP(hass, entry)
hass.data[DOMAIN][entry.unique_id] = hap
entry.runtime_data = hap
if not await hap.async_setup():
return False
@@ -107,12 +110,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: HomematicIPConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hap = entry.runtime_data
assert hap.reset_connection_listener is not None
hap = hass.data[DOMAIN].pop(entry.unique_id)
hap.reset_connection_listener()
await async_unload_services(hass)
@@ -122,7 +122,7 @@ async def async_unload_entry(
@callback
def _async_remove_obsolete_entities(
hass: HomeAssistant, entry: HomematicIPConfigEntry, hap: HomematicipHAP
hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP
):
"""Remove obsolete entities from entity registry."""

View File

@@ -11,12 +11,13 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .hap import AsyncHome, HomematicIPConfigEntry, HomematicipHAP
from .hap import AsyncHome, HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -25,11 +26,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP alrm control panel from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities([HomematicipAlarmControlPanelEntity(hap)])

View File

@@ -34,13 +34,14 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode"
ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
@@ -74,11 +75,11 @@ SAM_DEVICE_ATTRIBUTES = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = [HomematicipCloudConnectionSensor(hap)]
for device in hap.home.devices:
if isinstance(device, AccelerationSensor):

View File

@@ -5,20 +5,22 @@ from __future__ import annotations
from homematicip.device import WallMountedGarageDoorController
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP button from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipGarageDoorControllerButton(hap, device)

View File

@@ -24,6 +24,7 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@@ -31,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2}
COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5}
@@ -54,11 +55,11 @@ HMIP_ECO_CM = "ECO"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP climate from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipHeatingGroup(hap, device)

View File

@@ -21,11 +21,13 @@ from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
HMIP_COVER_OPEN = 0
HMIP_COVER_CLOSED = 1
@@ -35,11 +37,11 @@ HMIP_SLATS_CLOSED = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP cover from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = [
HomematicipCoverShutterGroup(hap, group)
for group in hap.home.groups

View File

@@ -1,11 +1,8 @@
"""Support for HomematicIP Cloud events."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from homematicip.base.channel_event import ChannelEvent
from homematicip.base.functionalChannels import FunctionalChannel
from homematicip.device import Device
from homeassistant.components.event import (
@@ -13,20 +10,19 @@ from homeassistant.components.event import (
EventEntity,
EventEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
@dataclass(frozen=True, kw_only=True)
class HmipEventEntityDescription(EventEntityDescription):
"""Description of a HomematicIP Cloud event."""
channel_event_types: list[str] | None = None
channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None
EVENT_DESCRIPTIONS = {
"doorbell": HmipEventEntityDescription(
@@ -34,42 +30,35 @@ EVENT_DESCRIPTIONS = {
translation_key="doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=["ring"],
channel_event_types=["DOOR_BELL_SENSOR_EVENT"],
channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT",
),
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP cover from a config entry."""
hap = config_entry.runtime_data
entities: list[HomematicipGenericEntity] = []
hap = hass.data[DOMAIN][config_entry.unique_id]
entities.extend(
async_add_entities(
HomematicipDoorBellEvent(
hap,
device,
channel.index,
description,
EVENT_DESCRIPTIONS["doorbell"],
)
for description in EVENT_DESCRIPTIONS.values()
for device in hap.home.devices
for channel in device.functionalChannels
if description.channel_selector_fn and description.channel_selector_fn(channel)
if channel.channelRole == "DOOR_BELL_INPUT"
)
async_add_entities(entities)
class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
"""Event class for HomematicIP doorbell events."""
_attr_device_class = EventDeviceClass.DOORBELL
entity_description: HmipEventEntityDescription
def __init__(
self,
@@ -97,27 +86,9 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
@callback
def _async_handle_event(self, *args, **kwargs) -> None:
"""Handle the event fired by the functional channel."""
raised_channel_event = self._get_channel_event_from_args(*args)
if not self._should_raise(raised_channel_event):
return
event_types = self.entity_description.event_types
if TYPE_CHECKING:
assert event_types is not None
self._trigger_event(event_type=event_types[0])
self.async_write_ha_state()
def _should_raise(self, event_type: str) -> bool:
"""Check if the event should be raised."""
if self.entity_description.channel_event_types is None:
return False
return event_type in self.entity_description.channel_event_types
def _get_channel_event_from_args(self, *args) -> str:
"""Get the channel event."""
if isinstance(args[0], ChannelEvent):
return args[0].channelEventType
return ""

View File

@@ -25,8 +25,6 @@ from .errors import HmipcConnectionError
_LOGGER = logging.getLogger(__name__)
type HomematicIPConfigEntry = ConfigEntry[HomematicipHAP]
async def build_context_async(
hass: HomeAssistant, hapid: str | None, authtoken: str | None
@@ -104,9 +102,7 @@ class HomematicipHAP:
home: AsyncHome
def __init__(
self, hass: HomeAssistant, config_entry: HomematicIPConfigEntry
) -> None:
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize HomematicIP Cloud connection."""
self.hass = hass
self.config_entry = config_entry

View File

@@ -28,20 +28,22 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP Cloud lights from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = []
for device in hap.home.devices:
if isinstance(device, BrandSwitchMeasuring):

View File

@@ -9,11 +9,12 @@ from homematicip.base.enums import LockState, MotorState
from homematicip.device import DoorLockDrive
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry
from .helpers import handle_errors
_LOGGER = logging.getLogger(__name__)
@@ -35,11 +36,11 @@ DEVICE_DLD_ATTRIBUTES = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP locks from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipDoorLockDrive(hap, device)

View File

@@ -44,6 +44,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
LIGHT_LUX,
@@ -60,8 +61,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
from .helpers import get_channels_from_device
ATTR_CURRENT_ILLUMINATION = "current_illumination"
@@ -94,11 +96,11 @@ ILLUMINATION_DEVICE_ATTRIBUTES = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP Cloud sensors from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = []
for device in hap.home.devices:
if isinstance(device, HomeControlAccessPoint):

View File

@@ -22,7 +22,6 @@ from homeassistant.helpers.service import (
)
from .const import DOMAIN
from .hap import HomematicIPConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -219,7 +218,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
async def async_unload_services(hass: HomeAssistant):
"""Unload HomematicIP Cloud services."""
if hass.config_entries.async_loaded_entries(DOMAIN):
if hass.data[DOMAIN]:
return
for hmipc_service in HMIPC_SERVICES:
@@ -236,9 +235,8 @@ async def _async_activate_eco_mode_with_duration(
if home := _get_home(hass, hapid):
await home.activate_absence_with_duration_async(duration)
else:
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_absence_with_duration_async(duration)
for hap in hass.data[DOMAIN].values():
await hap.home.activate_absence_with_duration_async(duration)
async def _async_activate_eco_mode_with_period(
@@ -251,9 +249,8 @@ async def _async_activate_eco_mode_with_period(
if home := _get_home(hass, hapid):
await home.activate_absence_with_period_async(endtime)
else:
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_absence_with_period_async(endtime)
for hap in hass.data[DOMAIN].values():
await hap.home.activate_absence_with_period_async(endtime)
async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
@@ -265,9 +262,8 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) ->
if home := _get_home(hass, hapid):
await home.activate_vacation_async(endtime, temperature)
else:
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_vacation_async(endtime, temperature)
for hap in hass.data[DOMAIN].values():
await hap.home.activate_vacation_async(endtime, temperature)
async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None:
@@ -276,9 +272,8 @@ async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall)
if home := _get_home(hass, hapid):
await home.deactivate_absence_async()
else:
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.deactivate_absence_async()
for hap in hass.data[DOMAIN].values():
await hap.home.deactivate_absence_async()
async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
@@ -287,9 +282,8 @@ async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall)
if home := _get_home(hass, hapid):
await home.deactivate_vacation_async()
else:
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.deactivate_vacation_async()
for hap in hass.data[DOMAIN].values():
await hap.home.deactivate_vacation_async()
async def _set_active_climate_profile(
@@ -299,15 +293,14 @@ async def _set_active_climate_profile(
entity_id_list = service.data[ATTR_ENTITY_ID]
climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
for hap in hass.data[DOMAIN].values():
if entity_id_list != "all":
for entity_id in entity_id_list:
group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
group = hap.hmip_device_by_entity_id.get(entity_id)
if group and isinstance(group, HeatingGroup):
await group.set_active_profile_async(climate_profile_index)
else:
for group in entry.runtime_data.home.groups:
for group in hap.home.groups:
if isinstance(group, HeatingGroup):
await group.set_active_profile_async(climate_profile_index)
@@ -320,10 +313,8 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N
config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
anonymize = service.data[ATTR_ANONYMIZE]
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
hap_sgtin = entry.unique_id
assert hap_sgtin is not None
for hap in hass.data[DOMAIN].values():
hap_sgtin = hap.config_entry.unique_id
if anonymize:
hap_sgtin = hap_sgtin[-4:]
@@ -332,7 +323,7 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N
path = Path(config_path)
config_file = path / file_name
json_state = await entry.runtime_data.home.download_configuration_async()
json_state = await hap.home.download_configuration_async()
json_state = handle_config(json_state, anonymize)
config_file.write_text(json_state, encoding="utf8")
@@ -342,15 +333,14 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall)
"""Service to reset the energy counter."""
entity_id_list = service.data[ATTR_ENTITY_ID]
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
for hap in hass.data[DOMAIN].values():
if entity_id_list != "all":
for entity_id in entity_id_list:
device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
device = hap.hmip_device_by_entity_id.get(entity_id)
if device and isinstance(device, SwitchMeasuring):
await device.reset_energy_counter_async()
else:
for device in entry.runtime_data.home.devices:
for device in hap.home.devices:
if isinstance(device, SwitchMeasuring):
await device.reset_energy_counter_async()
@@ -363,17 +353,14 @@ async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall
if home := _get_home(hass, hapid):
await home.set_cooling_async(cooling)
else:
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.set_cooling_async(cooling)
for hap in hass.data[DOMAIN].values():
await hap.home.set_cooling_async(cooling)
def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None:
"""Return a HmIP home."""
entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
if entry.unique_id == hapid:
return entry.runtime_data.home
if hap := hass.data[DOMAIN].get(hapid):
return hap.home
raise ServiceValidationError(
translation_domain=DOMAIN,

View File

@@ -23,20 +23,22 @@ from homematicip.device import (
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import ATTR_GROUP_MEMBER_UNREACHABLE, HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP switch from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = [
HomematicipGroupSwitch(hap, group)
for group in hap.home.groups

View File

@@ -18,12 +18,14 @@ from homeassistant.components.weather import (
ATTR_CONDITION_WINDY,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfSpeed, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
HOME_WEATHER_CONDITION = {
WeatherCondition.CLEAR: ATTR_CONDITION_SUNNY,
@@ -46,11 +48,11 @@ HOME_WEATHER_CONDITION = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP weather sensor from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = []
for device in hap.home.devices:
if isinstance(device, WeatherSensorPro):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any
@@ -57,8 +58,6 @@ SERVICE_SEND_COMMAND_SCHEMA = vol.Schema(
}
)
type HomeworksConfigEntry = ConfigEntry[HomeworksData]
@dataclass
class HomeworksData:
@@ -73,44 +72,45 @@ class HomeworksData:
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Lutron Homeworks Series 4 and 8 integration."""
async def async_call_service(service_call: ServiceCall) -> None:
"""Call the service."""
await async_send_command(hass, service_call.data)
hass.services.async_register(
DOMAIN,
"send_command",
async_send_command,
async_call_service,
schema=SERVICE_SEND_COMMAND_SCHEMA,
)
async def async_send_command(service_call: ServiceCall) -> None:
async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> None:
"""Send command to a controller."""
def get_controller_ids() -> list[str]:
"""Get homeworks data for the specified controller ID."""
return [
entry.runtime_data.controller_id
for entry in service_call.hass.config_entries.async_loaded_entries(DOMAIN)
]
return [data.controller_id for data in hass.data[DOMAIN].values()]
def get_homeworks_data(controller_id: str) -> HomeworksData | None:
"""Get homeworks data for the specified controller ID."""
entry: HomeworksConfigEntry
for entry in service_call.hass.config_entries.async_loaded_entries(DOMAIN):
if entry.runtime_data.controller_id == controller_id:
return entry.runtime_data
data: HomeworksData
for data in hass.data[DOMAIN].values():
if data.controller_id == controller_id:
return data
return None
homeworks_data = get_homeworks_data(service_call.data[CONF_CONTROLLER_ID])
homeworks_data = get_homeworks_data(data[CONF_CONTROLLER_ID])
if not homeworks_data:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_controller_id",
translation_placeholders={
"controller_id": service_call.data[CONF_CONTROLLER_ID],
"controller_id": data[CONF_CONTROLLER_ID],
"controller_ids": ",".join(get_controller_ids()),
},
)
commands = service_call.data[CONF_COMMAND]
commands = data[CONF_COMMAND]
_LOGGER.debug("Send commands: %s", commands)
for command in commands:
if command.lower().startswith("delay"):
@@ -119,7 +119,7 @@ async def async_send_command(service_call: ServiceCall) -> None:
await asyncio.sleep(delay / 1000)
else:
_LOGGER.debug("Sending command '%s'", command)
await service_call.hass.async_add_executor_job(
await hass.async_add_executor_job(
homeworks_data.controller._send, # noqa: SLF001
command,
)
@@ -132,9 +132,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: HomeworksConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Homeworks from a config entry."""
hass.data.setdefault(DOMAIN, {})
controller_id = entry.options[CONF_CONTROLLER_ID]
def hw_callback(msg_type: Any, values: Any) -> None:
@@ -173,7 +174,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeworksConfigEntry) ->
name = key_config[CONF_NAME]
keypads[addr] = HomeworksKeypad(hass, controller, controller_id, addr, name)
entry.runtime_data = HomeworksData(controller, controller_id, keypads)
hass.data[DOMAIN][entry.entry_id] = HomeworksData(
controller, controller_id, keypads
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
@@ -181,18 +184,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeworksConfigEntry) ->
return True
async def async_unload_entry(hass: HomeAssistant, entry: HomeworksConfigEntry) -> bool:
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):
for keypad in entry.runtime_data.keypads.values():
data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id)
for keypad in data.keypads.values():
keypad.unsubscribe()
await hass.async_add_executor_job(entry.runtime_data.controller.stop)
await hass.async_add_executor_job(data.controller.stop)
return unload_ok
async def update_listener(hass: HomeAssistant, entry: HomeworksConfigEntry) -> None:
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -8,13 +8,14 @@ from typing import Any
from pyhomeworks.pyhomeworks import HW_KEYPAD_LED_CHANGED, Homeworks
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeworksConfigEntry, HomeworksKeypad
from . import HomeworksData, HomeworksKeypad
from .const import (
CONF_ADDR,
CONF_BUTTONS,
@@ -31,11 +32,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: HomeworksConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Homeworks binary sensors."""
data = entry.runtime_data
data: HomeworksData = hass.data[DOMAIN][entry.entry_id]
controller = data.controller
controller_id = entry.options[CONF_CONTROLLER_ID]
entities = []

View File

@@ -7,12 +7,13 @@ import asyncio
from pyhomeworks.pyhomeworks import Homeworks
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeworksConfigEntry
from . import HomeworksData
from .const import (
CONF_ADDR,
CONF_BUTTONS,
@@ -27,11 +28,12 @@ from .entity import HomeworksEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: HomeworksConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Homeworks buttons."""
controller = entry.runtime_data.controller
data: HomeworksData = hass.data[DOMAIN][entry.entry_id]
controller = data.controller
controller_id = entry.options[CONF_CONTROLLER_ID]
entities = []
for keypad in entry.options.get(CONF_KEYPADS, []):

View File

@@ -8,13 +8,14 @@ from typing import Any
from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED, Homeworks
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeworksConfigEntry
from . import HomeworksData
from .const import CONF_ADDR, CONF_CONTROLLER_ID, CONF_DIMMERS, CONF_RATE, DOMAIN
from .entity import HomeworksEntity
@@ -23,11 +24,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: HomeworksConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Homeworks lights."""
controller = entry.runtime_data.controller
data: HomeworksData = hass.data[DOMAIN][entry.entry_id]
controller = data.controller
controller_id = entry.options[CONF_CONTROLLER_ID]
entities = []
for dimmer in entry.options.get(CONF_DIMMERS, []):

View File

@@ -3,17 +3,17 @@
from aiohue.util import normalize_bridge_id
from homeassistant.components import persistent_notification
from homeassistant.config_entries import SOURCE_IGNORE
from homeassistant.config_entries import SOURCE_IGNORE, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .bridge import HueBridge, HueConfigEntry
from .bridge import HueBridge
from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE
from .migration import check_migration
from .services import async_register_services
async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a bridge from a config entry."""
# check (and run) migrations if needed
await check_migration(hass, entry)
@@ -104,9 +104,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_success = await entry.runtime_data.async_reset()
if not hass.config_entries.async_loaded_entries(DOMAIN):
unload_success = await hass.data[DOMAIN][entry.entry_id].async_reset()
if len(hass.data[DOMAIN]) == 0:
hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE)
return unload_success

View File

@@ -2,21 +2,23 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .bridge import HueConfigEntry
from .bridge import HueBridge
from .const import DOMAIN
from .v1.binary_sensor import async_setup_entry as setup_entry_v1
from .v2.binary_sensor import async_setup_entry as setup_entry_v2
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HueConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensor entities."""
bridge = config_entry.runtime_data
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
if bridge.api_version == 1:
await setup_entry_v1(hass, config_entry, async_add_entities)
else:

View File

@@ -36,13 +36,11 @@ PLATFORMS_v2 = [
Platform.SWITCH,
]
type HueConfigEntry = ConfigEntry[HueBridge]
class HueBridge:
"""Manages a single Hue bridge."""
def __init__(self, hass: core.HomeAssistant, config_entry: HueConfigEntry) -> None:
def __init__(self, hass: core.HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the system."""
self.config_entry = config_entry
self.hass = hass
@@ -60,7 +58,7 @@ class HueBridge:
else:
self.api = HueBridgeV2(self.host, app_key)
# store (this) bridge object in hass data
self.config_entry.runtime_data = self
hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self
@property
def host(self) -> str:
@@ -165,7 +163,7 @@ class HueBridge:
)
if unload_success:
delattr(self.config_entry, "runtime_data")
self.hass.data[DOMAIN].pop(self.config_entry.entry_id)
return unload_success
@@ -181,7 +179,7 @@ class HueBridge:
create_config_flow(self.hass, self.host)
async def _update_listener(hass: core.HomeAssistant, entry: HueConfigEntry) -> None:
async def _update_listener(hass: core.HomeAssistant, entry: ConfigEntry) -> None:
"""Handle ConfigEntry options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -13,7 +13,12 @@ from aiohue.util import normalize_bridge_id
import slugify as unicode_slug
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST
from homeassistant.core import callback
from homeassistant.helpers import (
@@ -23,7 +28,6 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .bridge import HueConfigEntry
from .const import (
CONF_ALLOW_HUE_GROUPS,
CONF_ALLOW_UNREACHABLE,
@@ -49,7 +53,7 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: HueConfigEntry,
config_entry: ConfigEntry,
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
"""Get the options flow for this handler."""
if config_entry.data.get(CONF_API_VERSION, 1) == 1:

View File

@@ -26,15 +26,14 @@ if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from .bridge import HueConfigEntry
from .bridge import HueBridge
async def async_validate_trigger_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
entries: list[HueConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
if DOMAIN not in hass.data:
# happens at startup
return config
device_id = config[CONF_DEVICE_ID]
@@ -43,10 +42,10 @@ async def async_validate_trigger_config(
if (device_entry := dev_reg.async_get(device_id)) is None:
raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid")
for entry in entries:
if entry.entry_id not in device_entry.config_entries:
for conf_entry_id in device_entry.config_entries:
if conf_entry_id not in hass.data[DOMAIN]:
continue
bridge = entry.runtime_data
bridge: HueBridge = hass.data[DOMAIN][conf_entry_id]
if bridge.api_version == 1:
return await async_validate_trigger_config_v1(bridge, device_entry, config)
return await async_validate_trigger_config_v2(bridge, device_entry, config)
@@ -66,11 +65,10 @@ async def async_attach_trigger(
if (device_entry := dev_reg.async_get(device_id)) is None:
raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid")
entry: HueConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
if entry.entry_id not in device_entry.config_entries:
for conf_entry_id in device_entry.config_entries:
if conf_entry_id not in hass.data[DOMAIN]:
continue
bridge = entry.runtime_data
bridge: HueBridge = hass.data[DOMAIN][conf_entry_id]
if bridge.api_version == 1:
return await async_attach_trigger_v1(
bridge, device_entry, config, action, trigger_info
@@ -87,8 +85,7 @@ async def async_get_triggers(
hass: HomeAssistant, device_id: str
) -> list[dict[str, Any]]:
"""Get device triggers for given (hass) device id."""
entries: list[HueConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
if DOMAIN not in hass.data:
return []
# lookup device in HASS DeviceRegistry
dev_reg: dr.DeviceRegistry = dr.async_get(hass)
@@ -97,10 +94,10 @@ async def async_get_triggers(
# Iterate all config entries for this device
# and work out the bridge version
for entry in entries:
if entry.entry_id not in device_entry.config_entries:
for conf_entry_id in device_entry.config_entries:
if conf_entry_id not in hass.data[DOMAIN]:
continue
bridge = entry.runtime_data
bridge: HueBridge = hass.data[DOMAIN][conf_entry_id]
if bridge.api_version == 1:
return async_get_triggers_v1(bridge, device_entry)

View File

@@ -4,16 +4,18 @@ from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .bridge import HueConfigEntry
from .bridge import HueBridge
from .const import DOMAIN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: HueConfigEntry
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
bridge = entry.runtime_data
bridge: HueBridge = hass.data[DOMAIN][entry.entry_id]
if bridge.api_version == 1:
# diagnostics is only implemented for V2 bridges.
return {}

View File

@@ -14,21 +14,22 @@ from homeassistant.components.event import (
EventEntity,
EventEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .bridge import HueConfigEntry
from .const import DEFAULT_BUTTON_EVENT_TYPES, DEVICE_SPECIFIC_EVENT_TYPES
from .bridge import HueBridge
from .const import DEFAULT_BUTTON_EVENT_TYPES, DEVICE_SPECIFIC_EVENT_TYPES, DOMAIN
from .v2.entity import HueBaseEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HueConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up event platform from Hue button resources."""
bridge = config_entry.runtime_data
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
api: HueBridgeV2 = bridge.api
if bridge.api_version == 1:

Some files were not shown because too many files have changed in this diff Show More