mirror of
https://github.com/home-assistant/core.git
synced 2025-08-03 20:55:10 +02:00
Merge branch 'dev' into websocket_api_wait_integration
This commit is contained in:
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -653,7 +653,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Dependency review
|
- name: Dependency review
|
||||||
uses: actions/dependency-review-action@v4.5.0
|
uses: actions/dependency-review-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
license-check: false # We use our own license audit checks
|
license-check: false # We use our own license audit checks
|
||||||
|
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.28.13
|
uses: github/codeql-action/init@v3.28.15
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.28.13
|
uses: github/codeql-action/analyze@v3.28.15
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
@@ -859,8 +859,14 @@ async def _async_set_up_integrations(
|
|||||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||||
hass, config
|
hass, config
|
||||||
)
|
)
|
||||||
all_domains = set(all_integrations)
|
# Detect all cycles
|
||||||
domains = set(integrations)
|
integrations_after_dependencies = (
|
||||||
|
await loader.resolve_integrations_after_dependencies(
|
||||||
|
hass, all_integrations.values(), set(all_integrations)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
all_domains = set(integrations_after_dependencies)
|
||||||
|
domains = set(integrations) & all_domains
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Domains to be set up: %s | %s",
|
"Domains to be set up: %s | %s",
|
||||||
@@ -868,6 +874,8 @@ async def _async_set_up_integrations(
|
|||||||
all_domains - domains,
|
all_domains - domains,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_set_domains_to_be_loaded(hass, all_domains)
|
||||||
|
|
||||||
# Initialize recorder
|
# Initialize recorder
|
||||||
if "recorder" in all_domains:
|
if "recorder" in all_domains:
|
||||||
recorder.async_initialize_recorder(hass)
|
recorder.async_initialize_recorder(hass)
|
||||||
@@ -900,24 +908,12 @@ async def _async_set_up_integrations(
|
|||||||
stage_dep_domains_unfiltered = {
|
stage_dep_domains_unfiltered = {
|
||||||
dep
|
dep
|
||||||
for domain in stage_domains
|
for domain in stage_domains
|
||||||
for dep in all_integrations[domain].all_dependencies
|
for dep in integrations_after_dependencies[domain]
|
||||||
if dep not in stage_domains
|
if dep not in stage_domains
|
||||||
}
|
}
|
||||||
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
||||||
|
|
||||||
stage_all_domains = stage_domains | stage_dep_domains
|
stage_all_domains = stage_domains | stage_dep_domains
|
||||||
stage_all_integrations = {
|
|
||||||
domain: all_integrations[domain] for domain in stage_all_domains
|
|
||||||
}
|
|
||||||
# Detect all cycles
|
|
||||||
stage_integrations_after_dependencies = (
|
|
||||||
await loader.resolve_integrations_after_dependencies(
|
|
||||||
hass, stage_all_integrations.values(), stage_all_domains
|
|
||||||
)
|
|
||||||
)
|
|
||||||
stage_all_domains = set(stage_integrations_after_dependencies)
|
|
||||||
stage_domains &= stage_all_domains
|
|
||||||
stage_dep_domains &= stage_all_domains
|
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
||||||
@@ -928,8 +924,6 @@ async def _async_set_up_integrations(
|
|||||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
stage_dep_domains_unfiltered - stage_dep_domains,
|
||||||
)
|
)
|
||||||
|
|
||||||
async_set_domains_to_be_loaded(hass, stage_all_domains)
|
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||||
continue
|
continue
|
||||||
|
5
homeassistant/brands/eve.json
Normal file
5
homeassistant/brands/eve.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "eve",
|
||||||
|
"name": "Eve",
|
||||||
|
"iot_standards": ["matter"]
|
||||||
|
}
|
@@ -72,10 +72,10 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "Level",
|
"name": "Level",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "High",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "Moderate",
|
"moderate": "Moderate",
|
||||||
"very_high": "Very high"
|
"very_high": "[%key:common::state::very_high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,10 +89,10 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
"very_high": "[%key:common::state::very_high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,10 +123,10 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
"very_high": "[%key:common::state::very_high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,10 +167,10 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
"very_high": "[%key:common::state::very_high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +181,10 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
"very_high": "[%key:common::state::very_high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,10 +195,10 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
"very_high": "[%key:common::state::very_high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.9.9"]
|
"requirements": ["aioairzone==1.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,8 @@ from aioairzone.const import (
|
|||||||
AZD_HUMIDITY,
|
AZD_HUMIDITY,
|
||||||
AZD_TEMP,
|
AZD_TEMP,
|
||||||
AZD_TEMP_UNIT,
|
AZD_TEMP_UNIT,
|
||||||
|
AZD_THERMOSTAT_BATTERY,
|
||||||
|
AZD_THERMOSTAT_SIGNAL,
|
||||||
AZD_WEBSERVER,
|
AZD_WEBSERVER,
|
||||||
AZD_WIFI_RSSI,
|
AZD_WIFI_RSSI,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
@@ -73,6 +75,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
|||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
key=AZD_THERMOSTAT_BATTERY,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
key=AZD_THERMOSTAT_SIGNAL,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
translation_key="thermostat_signal",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -76,6 +76,9 @@
|
|||||||
"sensor": {
|
"sensor": {
|
||||||
"rssi": {
|
"rssi": {
|
||||||
"name": "RSSI"
|
"name": "RSSI"
|
||||||
|
},
|
||||||
|
"thermostat_signal": {
|
||||||
|
"name": "Signal strength"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -73,7 +73,7 @@ class AndroidTVRemoteBaseEntity(Entity):
|
|||||||
self._api.send_key_command(key_code, direction)
|
self._api.send_key_command(key_code, direction)
|
||||||
except ConnectionClosed as exc:
|
except ConnectionClosed as exc:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Connection to Android TV device is closed"
|
translation_domain=DOMAIN, translation_key="connection_closed"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
def _send_launch_app_command(self, app_link: str) -> None:
|
def _send_launch_app_command(self, app_link: str) -> None:
|
||||||
@@ -85,5 +85,5 @@ class AndroidTVRemoteBaseEntity(Entity):
|
|||||||
self._api.send_launch_app_command(app_link)
|
self._api.send_launch_app_command(app_link)
|
||||||
except ConnectionClosed as exc:
|
except ConnectionClosed as exc:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Connection to Android TV device is closed"
|
translation_domain=DOMAIN, translation_key="connection_closed"
|
||||||
) from exc
|
) from exc
|
||||||
|
@@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import AndroidTVRemoteConfigEntry
|
from . import AndroidTVRemoteConfigEntry
|
||||||
from .const import CONF_APP_ICON, CONF_APP_NAME
|
from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
|
||||||
from .entity import AndroidTVRemoteBaseEntity
|
from .entity import AndroidTVRemoteBaseEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -233,5 +233,5 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
|||||||
await asyncio.sleep(delay_secs)
|
await asyncio.sleep(delay_secs)
|
||||||
except ConnectionClosed as exc:
|
except ConnectionClosed as exc:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Connection to Android TV device is closed"
|
translation_domain=DOMAIN, translation_key="connection_closed"
|
||||||
) from exc
|
) from exc
|
||||||
|
@@ -54,5 +54,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"connection_closed": {
|
||||||
|
"message": "Connection to the Android TV device is closed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
SOURCE_IGNORE,
|
SOURCE_IGNORE,
|
||||||
|
SOURCE_REAUTH,
|
||||||
SOURCE_ZEROCONF,
|
SOURCE_ZEROCONF,
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
@@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_IDENTIFIERS: list(combined_identifiers),
|
CONF_IDENTIFIERS: list(combined_identifiers),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if entry.source != SOURCE_IGNORE:
|
# Don't reload ignored entries or in the middle of reauth,
|
||||||
|
# e.g. if the user is entering a new PIN
|
||||||
|
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
|
||||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||||
if not allow_exist:
|
if not allow_exist:
|
||||||
raise DeviceAlreadyConfigured
|
raise DeviceAlreadyConfigured
|
||||||
|
@@ -36,9 +36,9 @@
|
|||||||
"wi_fi_strength": {
|
"wi_fi_strength": {
|
||||||
"name": "Wi-Fi strength",
|
"name": "Wi-Fi strength",
|
||||||
"state": {
|
"state": {
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"medium": "Medium",
|
"medium": "[%key:common::state::medium%]",
|
||||||
"high": "High"
|
"high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -103,8 +103,8 @@
|
|||||||
"temperature_range": {
|
"temperature_range": {
|
||||||
"name": "Temperature range",
|
"name": "Temperature range",
|
||||||
"state": {
|
"state": {
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"high": "High"
|
"high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -124,8 +124,8 @@
|
|||||||
"battery": {
|
"battery": {
|
||||||
"name": "Battery",
|
"name": "Battery",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Normal",
|
"off": "[%key:common::state::normal%]",
|
||||||
"on": "Low"
|
"on": "[%key:common::state::low%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"battery_charging": {
|
"battery_charging": {
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"cold": {
|
"cold": {
|
||||||
"name": "Cold",
|
"name": "Cold",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]",
|
"off": "[%key:common::state::normal%]",
|
||||||
"on": "Cold"
|
"on": "Cold"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
"heat": {
|
"heat": {
|
||||||
"name": "Heat",
|
"name": "Heat",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]",
|
"off": "[%key:common::state::normal%]",
|
||||||
"on": "Hot"
|
"on": "Hot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
"bleak-retry-connector==3.9.0",
|
"bleak-retry-connector==3.9.0",
|
||||||
"bluetooth-adapters==0.21.4",
|
"bluetooth-adapters==0.21.4",
|
||||||
"bluetooth-auto-recovery==1.4.5",
|
"bluetooth-auto-recovery==1.4.5",
|
||||||
"bluetooth-data-tools==1.26.1",
|
"bluetooth-data-tools==1.27.0",
|
||||||
"dbus-fast==2.43.0",
|
"dbus-fast==2.43.0",
|
||||||
"habluetooth==3.37.0"
|
"habluetooth==3.37.0"
|
||||||
]
|
]
|
||||||
|
@@ -16,6 +16,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -91,11 +92,22 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
self._discovered[CONF_ACCESS_TOKEN] = token
|
self._discovered[CONF_ACCESS_TOKEN] = token
|
||||||
try:
|
try:
|
||||||
_, hub_name = await _validate_input(self.hass, self._discovered)
|
bond_id, hub_name = await _validate_input(self.hass, self._discovered)
|
||||||
except InputValidationError:
|
except InputValidationError:
|
||||||
return
|
return
|
||||||
|
await self.async_set_unique_id(bond_id)
|
||||||
|
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||||
self._discovered[CONF_NAME] = hub_name
|
self._discovered[CONF_NAME] = hub_name
|
||||||
|
|
||||||
|
async def async_step_dhcp(
|
||||||
|
self, discovery_info: DhcpServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by dhcp discovery."""
|
||||||
|
host = discovery_info.ip
|
||||||
|
bond_id = discovery_info.hostname.partition("-")[2].upper()
|
||||||
|
await self.async_set_unique_id(bond_id)
|
||||||
|
return await self.async_step_any_discovery(bond_id, host)
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
self, discovery_info: ZeroconfServiceInfo
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -104,11 +116,17 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
host: str = discovery_info.host
|
host: str = discovery_info.host
|
||||||
bond_id = name.partition(".")[0]
|
bond_id = name.partition(".")[0]
|
||||||
await self.async_set_unique_id(bond_id)
|
await self.async_set_unique_id(bond_id)
|
||||||
|
return await self.async_step_any_discovery(bond_id, host)
|
||||||
|
|
||||||
|
async def async_step_any_discovery(
|
||||||
|
self, bond_id: str, host: str
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by discovery."""
|
||||||
for entry in self._async_current_entries():
|
for entry in self._async_current_entries():
|
||||||
if entry.unique_id != bond_id:
|
if entry.unique_id != bond_id:
|
||||||
continue
|
continue
|
||||||
updates = {CONF_HOST: host}
|
updates = {CONF_HOST: host}
|
||||||
if entry.state == ConfigEntryState.SETUP_ERROR and (
|
if entry.state is ConfigEntryState.SETUP_ERROR and (
|
||||||
token := await async_get_token(self.hass, host)
|
token := await async_get_token(self.hass, host)
|
||||||
):
|
):
|
||||||
updates[CONF_ACCESS_TOKEN] = token
|
updates[CONF_ACCESS_TOKEN] = token
|
||||||
@@ -153,10 +171,14 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_HOST: self._discovered[CONF_HOST],
|
CONF_HOST: self._discovered[CONF_HOST],
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
_, hub_name = await _validate_input(self.hass, data)
|
bond_id, hub_name = await _validate_input(self.hass, data)
|
||||||
except InputValidationError as error:
|
except InputValidationError as error:
|
||||||
errors["base"] = error.base
|
errors["base"] = error.base
|
||||||
else:
|
else:
|
||||||
|
await self.async_set_unique_id(bond_id)
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates={CONF_HOST: self._discovered[CONF_HOST]}
|
||||||
|
)
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=hub_name,
|
title=hub_name,
|
||||||
data=data,
|
data=data,
|
||||||
@@ -185,8 +207,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except InputValidationError as error:
|
except InputValidationError as error:
|
||||||
errors["base"] = error.base
|
errors["base"] = error.base
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(bond_id)
|
await self.async_set_unique_id(bond_id, raise_on_progress=False)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured(
|
||||||
|
updates={CONF_HOST: user_input[CONF_HOST]}
|
||||||
|
)
|
||||||
return self.async_create_entry(title=hub_name, data=user_input)
|
return self.async_create_entry(title=hub_name, data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@@ -3,6 +3,16 @@
|
|||||||
"name": "Bond",
|
"name": "Bond",
|
||||||
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
|
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"hostname": "bond-*",
|
||||||
|
"macaddress": "3C6A2C1*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostname": "bond-*",
|
||||||
|
"macaddress": "F44E38*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bond",
|
"documentation": "https://www.home-assistant.io/integrations/bond",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["bond_async"],
|
"loggers": ["bond_async"],
|
||||||
|
@@ -9,12 +9,12 @@ from bosch_alarm_mode2 import Panel
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL]
|
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR]
|
||||||
|
|
||||||
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
||||||
|
|
||||||
@@ -34,10 +34,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
|
|||||||
await panel.connect()
|
await panel.connect()
|
||||||
except (PermissionError, ValueError) as err:
|
except (PermissionError, ValueError) as err:
|
||||||
await panel.disconnect()
|
await panel.disconnect()
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||||
|
) from err
|
||||||
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
|
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
|
||||||
await panel.disconnect()
|
await panel.disconnect()
|
||||||
raise ConfigEntryNotReady("Connection failed") from err
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect",
|
||||||
|
) from err
|
||||||
|
|
||||||
entry.runtime_data = panel
|
entry.runtime_data = panel
|
||||||
|
|
||||||
|
@@ -10,11 +10,10 @@ from homeassistant.components.alarm_control_panel import (
|
|||||||
AlarmControlPanelState,
|
AlarmControlPanelState,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import BoschAlarmConfigEntry
|
from . import BoschAlarmConfigEntry
|
||||||
from .const import DOMAIN
|
from .entity import BoschAlarmAreaEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -35,7 +34,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AreaAlarmControlPanel(AlarmControlPanelEntity):
|
class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
|
||||||
"""An alarm control panel entity for a bosch alarm panel."""
|
"""An alarm control panel entity for a bosch alarm panel."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
@@ -48,19 +47,8 @@ class AreaAlarmControlPanel(AlarmControlPanelEntity):
|
|||||||
|
|
||||||
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||||
"""Initialise a Bosch Alarm control panel entity."""
|
"""Initialise a Bosch Alarm control panel entity."""
|
||||||
self.panel = panel
|
super().__init__(panel, area_id, unique_id, False, False, True)
|
||||||
self._area = panel.areas[area_id]
|
self._attr_unique_id = self._area_unique_id
|
||||||
self._area_id = area_id
|
|
||||||
self._attr_unique_id = f"{unique_id}_area_{area_id}"
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
|
||||||
name=self._area.name,
|
|
||||||
manufacturer="Bosch Security Systems",
|
|
||||||
via_device=(
|
|
||||||
DOMAIN,
|
|
||||||
unique_id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alarm_state(self) -> AlarmControlPanelState | None:
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
@@ -90,20 +78,3 @@ class AreaAlarmControlPanel(AlarmControlPanelEntity):
|
|||||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
await self.panel.area_arm_all(self._area_id)
|
await self.panel.area_arm_all(self._area_id)
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return True if entity is available."""
|
|
||||||
return self.panel.connection_status()
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Run when entity attached to hass."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
self._area.status_observer.attach(self.schedule_update_ha_state)
|
|
||||||
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Run when entity removed from hass."""
|
|
||||||
await super().async_will_remove_from_hass()
|
|
||||||
self._area.status_observer.detach(self.schedule_update_ha_state)
|
|
||||||
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
import ssl
|
import ssl
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -10,7 +11,12 @@ from typing import Any
|
|||||||
from bosch_alarm_mode2 import Panel
|
from bosch_alarm_mode2 import Panel
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import (
|
||||||
|
SOURCE_RECONFIGURE,
|
||||||
|
SOURCE_USER,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CODE,
|
CONF_CODE,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
@@ -107,6 +113,13 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
self._data = user_input
|
self._data = user_input
|
||||||
self._data[CONF_MODEL] = model
|
self._data[CONF_MODEL] = model
|
||||||
|
|
||||||
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
|
if (
|
||||||
|
self._get_reconfigure_entry().data[CONF_MODEL]
|
||||||
|
!= self._data[CONF_MODEL]
|
||||||
|
):
|
||||||
|
return self.async_abort(reason="device_mismatch")
|
||||||
return await self.async_step_auth()
|
return await self.async_step_auth()
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
@@ -116,6 +129,12 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the reconfigure step."""
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
async def async_step_auth(
|
async def async_step_auth(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -153,13 +172,77 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
if serial_number:
|
if serial_number:
|
||||||
await self.async_set_unique_id(str(serial_number))
|
await self.async_set_unique_id(str(serial_number))
|
||||||
self._abort_if_unique_id_configured()
|
if self.source == SOURCE_USER:
|
||||||
else:
|
if serial_number:
|
||||||
self._async_abort_entries_match({CONF_HOST: self._data[CONF_HOST]})
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(title=f"Bosch {model}", data=self._data)
|
else:
|
||||||
|
self._async_abort_entries_match(
|
||||||
|
{CONF_HOST: self._data[CONF_HOST]}
|
||||||
|
)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Bosch {model}", data=self._data
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="auth",
|
step_id="auth",
|
||||||
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform reauth upon an authentication error."""
|
||||||
|
self._data = dict(entry_data)
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the reauth step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
# Each model variant requires a different authentication flow
|
||||||
|
if "Solution" in self._data[CONF_MODEL]:
|
||||||
|
schema = STEP_AUTH_DATA_SCHEMA_SOLUTION
|
||||||
|
elif "AMAX" in self._data[CONF_MODEL]:
|
||||||
|
schema = STEP_AUTH_DATA_SCHEMA_AMAX
|
||||||
|
else:
|
||||||
|
schema = STEP_AUTH_DATA_SCHEMA_BG
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
self._data.update(user_input)
|
||||||
|
try:
|
||||||
|
(_, _) = await try_connect(self._data, Panel.LOAD_EXTENDED_INFO)
|
||||||
|
except (PermissionError, ValueError) as e:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
_LOGGER.error("Authentication Error: %s", e)
|
||||||
|
except (
|
||||||
|
OSError,
|
||||||
|
ConnectionRefusedError,
|
||||||
|
ssl.SSLError,
|
||||||
|
TimeoutError,
|
||||||
|
) as e:
|
||||||
|
_LOGGER.error("Connection Error: %s", e)
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
reauth_entry,
|
||||||
|
data_updates=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
73
homeassistant/components/bosch_alarm/diagnostics.py
Normal file
73
homeassistant/components/bosch_alarm/diagnostics.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""Diagnostics for bosch alarm."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import BoschAlarmConfigEntry
|
||||||
|
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
|
||||||
|
|
||||||
|
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: BoschAlarmConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||||
|
"data": {
|
||||||
|
"model": entry.runtime_data.model,
|
||||||
|
"serial_number": entry.runtime_data.serial_number,
|
||||||
|
"protocol_version": entry.runtime_data.protocol_version,
|
||||||
|
"firmware_version": entry.runtime_data.firmware_version,
|
||||||
|
"areas": [
|
||||||
|
{
|
||||||
|
"id": area_id,
|
||||||
|
"name": area.name,
|
||||||
|
"all_ready": area.all_ready,
|
||||||
|
"part_ready": area.part_ready,
|
||||||
|
"faults": area.faults,
|
||||||
|
"alarms": area.alarms,
|
||||||
|
"disarmed": area.is_disarmed(),
|
||||||
|
"arming": area.is_arming(),
|
||||||
|
"pending": area.is_pending(),
|
||||||
|
"part_armed": area.is_part_armed(),
|
||||||
|
"all_armed": area.is_all_armed(),
|
||||||
|
"armed": area.is_armed(),
|
||||||
|
"triggered": area.is_triggered(),
|
||||||
|
}
|
||||||
|
for area_id, area in entry.runtime_data.areas.items()
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"id": point_id,
|
||||||
|
"name": point.name,
|
||||||
|
"open": point.is_open(),
|
||||||
|
"normal": point.is_normal(),
|
||||||
|
}
|
||||||
|
for point_id, point in entry.runtime_data.points.items()
|
||||||
|
],
|
||||||
|
"doors": [
|
||||||
|
{
|
||||||
|
"id": door_id,
|
||||||
|
"name": door.name,
|
||||||
|
"open": door.is_open(),
|
||||||
|
"locked": door.is_locked(),
|
||||||
|
}
|
||||||
|
for door_id, door in entry.runtime_data.doors.items()
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": output_id,
|
||||||
|
"name": output.name,
|
||||||
|
"active": output.is_active(),
|
||||||
|
}
|
||||||
|
for output_id, output in entry.runtime_data.outputs.items()
|
||||||
|
],
|
||||||
|
"history_events": entry.runtime_data.events,
|
||||||
|
},
|
||||||
|
}
|
88
homeassistant/components/bosch_alarm/entity.py
Normal file
88
homeassistant/components/bosch_alarm/entity.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""Support for Bosch Alarm Panel History as a sensor."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bosch_alarm_mode2 import Panel
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import Entity
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BoschAlarmEntity(Entity):
|
||||||
|
"""A base entity for a bosch alarm panel."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, panel: Panel, unique_id: str) -> None:
|
||||||
|
"""Set up a entity for a bosch alarm panel."""
|
||||||
|
self.panel = panel
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, unique_id)},
|
||||||
|
name=f"Bosch {panel.model}",
|
||||||
|
manufacturer="Bosch Security Systems",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.panel.connection_status()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Observe state changes."""
|
||||||
|
self.panel.connection_status_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)
|
||||||
|
|
||||||
|
|
||||||
|
class BoschAlarmAreaEntity(BoschAlarmEntity):
|
||||||
|
"""A base entity for area related entities within a bosch alarm panel."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
panel: Panel,
|
||||||
|
area_id: int,
|
||||||
|
unique_id: str,
|
||||||
|
observe_alarms: bool,
|
||||||
|
observe_ready: bool,
|
||||||
|
observe_status: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a area related entity for a bosch alarm panel."""
|
||||||
|
super().__init__(panel, unique_id)
|
||||||
|
self._area_id = area_id
|
||||||
|
self._area_unique_id = f"{unique_id}_area_{area_id}"
|
||||||
|
self._observe_alarms = observe_alarms
|
||||||
|
self._observe_ready = observe_ready
|
||||||
|
self._observe_status = observe_status
|
||||||
|
self._area = panel.areas[area_id]
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._area_unique_id)},
|
||||||
|
name=self._area.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()
|
||||||
|
if self._observe_alarms:
|
||||||
|
self._area.alarm_observer.attach(self.schedule_update_ha_state)
|
||||||
|
if self._observe_ready:
|
||||||
|
self._area.ready_observer.attach(self.schedule_update_ha_state)
|
||||||
|
if self._observe_status:
|
||||||
|
self._area.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()
|
||||||
|
if self._observe_alarms:
|
||||||
|
self._area.alarm_observer.detach(self.schedule_update_ha_state)
|
||||||
|
if self._observe_ready:
|
||||||
|
self._area.ready_observer.detach(self.schedule_update_ha_state)
|
||||||
|
if self._observe_status:
|
||||||
|
self._area.status_observer.detach(self.schedule_update_ha_state)
|
9
homeassistant/components/bosch_alarm/icons.json
Normal file
9
homeassistant/components/bosch_alarm/icons.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"faulting_points": {
|
||||||
|
"default": "mdi:alert-circle-outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["bosch-alarm-mode2==0.4.3"]
|
"requirements": ["bosch-alarm-mode2==0.4.6"]
|
||||||
}
|
}
|
||||||
|
@@ -40,7 +40,7 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: todo
|
log-when-unavailable: todo
|
||||||
parallel-updates: todo
|
parallel-updates: todo
|
||||||
reauthentication-flow: todo
|
reauthentication-flow: done
|
||||||
test-coverage: done
|
test-coverage: done
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
@@ -62,9 +62,9 @@ rules:
|
|||||||
entity-category: todo
|
entity-category: todo
|
||||||
entity-device-class: todo
|
entity-device-class: todo
|
||||||
entity-disabled-by-default: todo
|
entity-disabled-by-default: todo
|
||||||
entity-translations: todo
|
entity-translations: done
|
||||||
exception-translations: todo
|
exception-translations: todo
|
||||||
icon-translations: todo
|
icon-translations: done
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: todo
|
||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
|
86
homeassistant/components/bosch_alarm/sensor.py
Normal file
86
homeassistant/components/bosch_alarm/sensor.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""Support for Bosch Alarm Panel History as a sensor."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from bosch_alarm_mode2 import Panel
|
||||||
|
from bosch_alarm_mode2.panel import Area
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import BoschAlarmConfigEntry
|
||||||
|
from .entity import BoschAlarmAreaEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Bosch Alarm sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Area], int]
|
||||||
|
observe_alarms: bool = False
|
||||||
|
observe_ready: bool = False
|
||||||
|
observe_status: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
|
||||||
|
BoschAlarmSensorEntityDescription(
|
||||||
|
key="faulting_points",
|
||||||
|
translation_key="faulting_points",
|
||||||
|
value_fn=lambda area: area.faults,
|
||||||
|
observe_ready=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BoschAlarmConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up bosch alarm sensors."""
|
||||||
|
|
||||||
|
panel = config_entry.runtime_data
|
||||||
|
unique_id = config_entry.unique_id or config_entry.entry_id
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
BoschAreaSensor(panel, area_id, unique_id, template)
|
||||||
|
for area_id in panel.areas
|
||||||
|
for template in SENSOR_TYPES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
|
||||||
|
"""An area sensor entity for a bosch alarm panel."""
|
||||||
|
|
||||||
|
entity_description: BoschAlarmSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
panel: Panel,
|
||||||
|
area_id: int,
|
||||||
|
unique_id: str,
|
||||||
|
entity_description: BoschAlarmSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Set up an area sensor entity for a bosch alarm panel."""
|
||||||
|
super().__init__(
|
||||||
|
panel,
|
||||||
|
area_id,
|
||||||
|
unique_id,
|
||||||
|
entity_description.observe_alarms,
|
||||||
|
entity_description.observe_ready,
|
||||||
|
entity_description.observe_status,
|
||||||
|
)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> int:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self._area)
|
@@ -22,6 +22,18 @@
|
|||||||
"installer_code": "The installer code from your panel",
|
"installer_code": "The installer code from your panel",
|
||||||
"user_code": "The user code from your panel"
|
"user_code": "The user code from your panel"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data::installer_code%]",
|
||||||
|
"user_code": "[%key:component::bosch_alarm::config::step::auth::data::user_code%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "[%key:component::bosch_alarm::config::step::auth::data_description::password%]",
|
||||||
|
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data_description::installer_code%]",
|
||||||
|
"user_code": "[%key:component::bosch_alarm::config::step::auth::data_description::user_code%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -30,7 +42,26 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"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%]",
|
||||||
|
"device_mismatch": "Please ensure you reconfigure against the same device."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"cannot_connect": {
|
||||||
|
"message": "Could not connect to panel."
|
||||||
|
},
|
||||||
|
"authentication_failed": {
|
||||||
|
"message": "Incorrect credentials for panel."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"faulting_points": {
|
||||||
|
"name": "Faulting points",
|
||||||
|
"unit_of_measurement": "points"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -74,7 +74,7 @@
|
|||||||
},
|
},
|
||||||
"get_events": {
|
"get_events": {
|
||||||
"name": "Get events",
|
"name": "Get events",
|
||||||
"description": "Get events on a calendar within a time range.",
|
"description": "Retrieves events on a calendar within a time range.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"start_date_time": {
|
"start_date_time": {
|
||||||
"name": "Start time",
|
"name": "Start time",
|
||||||
|
@@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
|
|||||||
flow_id=flow_id, user_input=tokens
|
flow_id=flow_id, user_input=tokens
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.async_create_task(await_tokens())
|
# It's a background task because it should be cancelled on shutdown and there's nothing else
|
||||||
|
# we can do in such case. There's also no need to wait for this during setup.
|
||||||
|
self.hass.async_create_background_task(
|
||||||
|
await_tokens(), name="Awaiting OAuth tokens"
|
||||||
|
)
|
||||||
|
|
||||||
return authorize_url
|
return authorize_url
|
||||||
|
|
||||||
|
@@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
|||||||
|
|
||||||
async def async_set_humidity(self, humidity: int) -> None:
|
async def async_set_humidity(self, humidity: int) -> None:
|
||||||
"""Set new target humidity."""
|
"""Set new target humidity."""
|
||||||
if self.mode == HumidifierComelitMode.OFF:
|
if not self._attr_is_on:
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="humidity_while_off",
|
translation_key="humidity_while_off",
|
||||||
@@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
|||||||
await self.coordinator.api.set_humidity_status(
|
await self.coordinator.api.set_humidity_status(
|
||||||
self._device.index, self._set_command
|
self._device.index, self._set_command
|
||||||
)
|
)
|
||||||
|
self._attr_is_on = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn off."""
|
"""Turn off."""
|
||||||
await self.coordinator.api.set_humidity_status(
|
await self.coordinator.api.set_humidity_status(
|
||||||
self._device.index, HumidifierComelitCommand.OFF
|
self._device.index, HumidifierComelitCommand.OFF
|
||||||
)
|
)
|
||||||
|
self._attr_is_on = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
@@ -52,7 +52,9 @@
|
|||||||
"rest": "Rest",
|
"rest": "Rest",
|
||||||
"sabotated": "Sabotated"
|
"sabotated": "Sabotated"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
"humidifier": {
|
||||||
"humidifier": {
|
"humidifier": {
|
||||||
"name": "Humidifier"
|
"name": "Humidifier"
|
||||||
},
|
},
|
||||||
|
@@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||||
|
from homeassistant.util.ssl import client_context_no_verify
|
||||||
|
|
||||||
from .const import KEY_MAC, TIMEOUT
|
from .const import KEY_MAC, TIMEOUT
|
||||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||||
@@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
|
|||||||
key=entry.data.get(CONF_API_KEY),
|
key=entry.data.get(CONF_API_KEY),
|
||||||
uuid=entry.data.get(CONF_UUID),
|
uuid=entry.data.get(CONF_UUID),
|
||||||
password=entry.data.get(CONF_PASSWORD),
|
password=entry.data.get(CONF_PASSWORD),
|
||||||
|
ssl_context=client_context_no_verify(),
|
||||||
)
|
)
|
||||||
_LOGGER.debug("Connection to %s successful", host)
|
_LOGGER.debug("Connection to %s successful", host)
|
||||||
except TimeoutError as err:
|
except TimeoutError as err:
|
||||||
|
@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|||||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID
|
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
from homeassistant.util.ssl import client_context_no_verify
|
||||||
|
|
||||||
from .const import DOMAIN, KEY_MAC, TIMEOUT
|
from .const import DOMAIN, KEY_MAC, TIMEOUT
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
key=key,
|
key=key,
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
password=password,
|
password=password,
|
||||||
|
ssl_context=client_context_no_verify(),
|
||||||
)
|
)
|
||||||
except (TimeoutError, ClientError):
|
except (TimeoutError, ClientError):
|
||||||
self.host = None
|
self.host = None
|
||||||
|
@@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pydaikin"],
|
"loggers": ["pydaikin"],
|
||||||
"requirements": ["pydaikin==2.14.1"],
|
"requirements": ["pydaikin==2.15.0"],
|
||||||
"zeroconf": ["_dkapi._tcp.local."]
|
"zeroconf": ["_dkapi._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["dsmr_parser"],
|
"loggers": ["dsmr_parser"],
|
||||||
"requirements": ["dsmr-parser==1.4.2"]
|
"requirements": ["dsmr-parser==1.4.3"]
|
||||||
}
|
}
|
||||||
|
@@ -51,8 +51,8 @@
|
|||||||
"electricity_active_tariff": {
|
"electricity_active_tariff": {
|
||||||
"name": "Active tariff",
|
"name": "Active tariff",
|
||||||
"state": {
|
"state": {
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"normal": "Normal"
|
"normal": "[%key:common::state::normal%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electricity_delivered_tariff_1": {
|
"electricity_delivered_tariff_1": {
|
||||||
|
@@ -140,8 +140,8 @@
|
|||||||
"electricity_tariff": {
|
"electricity_tariff": {
|
||||||
"name": "Electricity tariff",
|
"name": "Electricity tariff",
|
||||||
"state": {
|
"state": {
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"high": "High"
|
"high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"power_failure_count": {
|
"power_failure_count": {
|
||||||
|
@@ -55,7 +55,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
"description": "Ecobee thermostat on which to create the vacation."
|
"description": "ecobee thermostat on which to create the vacation."
|
||||||
},
|
},
|
||||||
"vacation_name": {
|
"vacation_name": {
|
||||||
"name": "Vacation name",
|
"name": "Vacation name",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
"description": "Ecobee thermostat on which to delete the vacation."
|
"description": "ecobee thermostat on which to delete the vacation."
|
||||||
},
|
},
|
||||||
"vacation_name": {
|
"vacation_name": {
|
||||||
"name": "[%key:component::ecobee::services::create_vacation::fields::vacation_name::name%]",
|
"name": "[%key:component::ecobee::services::create_vacation::fields::vacation_name::name%]",
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
},
|
},
|
||||||
"set_mic_mode": {
|
"set_mic_mode": {
|
||||||
"name": "Set mic mode",
|
"name": "Set mic mode",
|
||||||
"description": "Enables/disables Alexa microphone (only for Ecobee 4).",
|
"description": "Enables/disables Alexa microphone (only for ecobee 4).",
|
||||||
"fields": {
|
"fields": {
|
||||||
"mic_enabled": {
|
"mic_enabled": {
|
||||||
"name": "Mic enabled",
|
"name": "Mic enabled",
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
"description": "Ecobee thermostat on which to set active sensors."
|
"description": "ecobee thermostat on which to set active sensors."
|
||||||
},
|
},
|
||||||
"preset_mode": {
|
"preset_mode": {
|
||||||
"name": "Climate Name",
|
"name": "Climate Name",
|
||||||
@@ -203,12 +203,12 @@
|
|||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"migrate_aux_heat": {
|
"migrate_aux_heat": {
|
||||||
"title": "Migration of Ecobee set_aux_heat action",
|
"title": "Migration of ecobee set_aux_heat action",
|
||||||
"fix_flow": {
|
"fix_flow": {
|
||||||
"step": {
|
"step": {
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"description": "The Ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
"description": "The ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
||||||
"title": "Disable legacy Ecobee set_aux_heat action"
|
"title": "Disable legacy ecobee set_aux_heat action"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
|
||||||
}
|
}
|
||||||
|
@@ -176,9 +176,9 @@
|
|||||||
"water_amount": {
|
"water_amount": {
|
||||||
"name": "Water flow level",
|
"name": "Water flow level",
|
||||||
"state": {
|
"state": {
|
||||||
"high": "High",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"medium": "Medium",
|
"medium": "[%key:common::state::medium%]",
|
||||||
"ultrahigh": "Ultrahigh"
|
"ultrahigh": "Ultrahigh"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -9,7 +9,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT]
|
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
18
homeassistant/components/eheimdigital/icons.json
Normal file
18
homeassistant/components/eheimdigital/icons.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"current_speed": {
|
||||||
|
"default": "mdi:pump"
|
||||||
|
},
|
||||||
|
"service_hours": {
|
||||||
|
"default": "mdi:wrench-clock"
|
||||||
|
},
|
||||||
|
"error_code": {
|
||||||
|
"default": "mdi:alert-octagon",
|
||||||
|
"state": {
|
||||||
|
"no_error": "mdi:check-circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
homeassistant/components/eheimdigital/sensor.py
Normal file
114
homeassistant/components/eheimdigital/sensor.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""EHEIM Digital sensors."""
|
||||||
|
|
||||||
|
from collections.abc import 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 FilterErrorCode
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||||
|
from homeassistant.components.sensor.const import SensorDeviceClass
|
||||||
|
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||||
|
from .entity import EheimDigitalEntity
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
|
||||||
|
"""Class describing EHEIM Digital sensor entities."""
|
||||||
|
|
||||||
|
value_fn: Callable[[_DeviceT_co], float | str | None]
|
||||||
|
|
||||||
|
|
||||||
|
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||||
|
EheimDigitalSensorDescription[EheimDigitalClassicVario], ...
|
||||||
|
] = (
|
||||||
|
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||||
|
key="current_speed",
|
||||||
|
translation_key="current_speed",
|
||||||
|
value_fn=lambda device: device.current_speed,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
),
|
||||||
|
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||||
|
key="service_hours",
|
||||||
|
translation_key="service_hours",
|
||||||
|
value_fn=lambda device: device.service_hours,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.DAYS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||||
|
key="error_code",
|
||||||
|
translation_key="error_code",
|
||||||
|
value_fn=(
|
||||||
|
lambda device: device.error_code.name.lower()
|
||||||
|
if device.error_code is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[name.lower() for name in FilterErrorCode._member_names_],
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: EheimDigitalConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the callbacks for the coordinator so lights 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 light entities for one or multiple devices."""
|
||||||
|
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
|
||||||
|
for device in device_address.values():
|
||||||
|
if isinstance(device, EheimDigitalClassicVario):
|
||||||
|
entities += [
|
||||||
|
EheimDigitalSensor[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 EheimDigitalSensor(
|
||||||
|
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
|
||||||
|
):
|
||||||
|
"""Represent a EHEIM Digital sensor entity."""
|
||||||
|
|
||||||
|
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: EheimDigitalUpdateCoordinator,
|
||||||
|
device: _DeviceT_co,
|
||||||
|
description: EheimDigitalSensorDescription[_DeviceT_co],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize an EHEIM Digital number entity."""
|
||||||
|
super().__init__(coordinator, device)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
self._attr_native_value = self.entity_description.value_fn(self._device)
|
@@ -46,6 +46,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"current_speed": {
|
||||||
|
"name": "Current speed"
|
||||||
|
},
|
||||||
|
"service_hours": {
|
||||||
|
"name": "Remaining hours until service"
|
||||||
|
},
|
||||||
|
"error_code": {
|
||||||
|
"name": "Error code",
|
||||||
|
"state": {
|
||||||
|
"no_error": "No error",
|
||||||
|
"rotor_stuck": "Rotor stuck",
|
||||||
|
"air_in_filter": "Air in filter"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,13 @@ from homeassistant.config_entries import (
|
|||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlow,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_TOKEN,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.httpx_client import get_async_client
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
@@ -40,6 +46,13 @@ CONF_SERIAL = "serial"
|
|||||||
|
|
||||||
INSTALLER_AUTH_USERNAME = "installer"
|
INSTALLER_AUTH_USERNAME = "installer"
|
||||||
|
|
||||||
|
AVOID_REFLECT_KEYS = {CONF_PASSWORD, CONF_TOKEN}
|
||||||
|
|
||||||
|
|
||||||
|
def without_avoid_reflect_keys(dictionary: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Return a dictionary without AVOID_REFLECT_KEYS."""
|
||||||
|
return {k: v for k, v in dictionary.items() if k not in AVOID_REFLECT_KEYS}
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(
|
async def validate_input(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -205,7 +218,10 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
description_placeholders["serial"] = serial
|
description_placeholders["serial"] = serial
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
data_schema=self._async_generate_schema(),
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
self._async_generate_schema(),
|
||||||
|
without_avoid_reflect_keys(user_input or reauth_entry.data),
|
||||||
|
),
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
@@ -259,10 +275,12 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_SERIAL: self.unique_id,
|
CONF_SERIAL: self.unique_id,
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=self._async_generate_schema(),
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
self._async_generate_schema(),
|
||||||
|
without_avoid_reflect_keys(user_input or {}),
|
||||||
|
),
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
@@ -306,11 +324,11 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
description_placeholders["serial"] = serial
|
description_placeholders["serial"] = serial
|
||||||
|
|
||||||
suggested_values: Mapping[str, Any] = user_input or reconfigure_entry.data
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reconfigure",
|
step_id="reconfigure",
|
||||||
data_schema=self.add_suggested_values_to_schema(
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
self._async_generate_schema(), suggested_values
|
self._async_generate_schema(),
|
||||||
|
without_avoid_reflect_keys(user_input or reconfigure_entry.data),
|
||||||
),
|
),
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
|
@@ -66,16 +66,19 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
for end_point in end_points:
|
for end_point in end_points:
|
||||||
response = await envoy.request(end_point)
|
try:
|
||||||
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
response = await envoy.request(end_point)
|
||||||
serial, CLEAN_TEXT
|
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
||||||
)
|
serial, CLEAN_TEXT
|
||||||
fixture_data[f"{end_point}_log"] = json_dumps(
|
)
|
||||||
{
|
fixture_data[f"{end_point}_log"] = json_dumps(
|
||||||
"headers": dict(response.headers.items()),
|
{
|
||||||
"code": response.status_code,
|
"headers": dict(response.headers.items()),
|
||||||
}
|
"code": response.status_code,
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
except EnvoyError as err:
|
||||||
|
fixture_data[f"{end_point}_log"] = {"Error": repr(err)}
|
||||||
return fixture_data
|
return fixture_data
|
||||||
|
|
||||||
|
|
||||||
@@ -160,10 +163,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
|
|
||||||
fixture_data: dict[str, Any] = {}
|
fixture_data: dict[str, Any] = {}
|
||||||
if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False):
|
if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False):
|
||||||
try:
|
fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial)
|
||||||
fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial)
|
|
||||||
except EnvoyError as err:
|
|
||||||
fixture_data["Error"] = repr(err)
|
|
||||||
|
|
||||||
diagnostic_data: dict[str, Any] = {
|
diagnostic_data: dict[str, Any] = {
|
||||||
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyenphase"],
|
"loggers": ["pyenphase"],
|
||||||
"requirements": ["pyenphase==1.25.1"],
|
"requirements": ["pyenphase==1.25.5"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_enphase-envoy._tcp.local."
|
"type": "_enphase-envoy._tcp.local."
|
||||||
|
@@ -22,5 +22,5 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["eq3btsmart"],
|
"loggers": ["eq3btsmart"],
|
||||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.12.0"]
|
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.13.1"]
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ from aioesphomeapi import (
|
|||||||
APIConnectionError,
|
APIConnectionError,
|
||||||
APIVersion,
|
APIVersion,
|
||||||
DeviceInfo as EsphomeDeviceInfo,
|
DeviceInfo as EsphomeDeviceInfo,
|
||||||
EncryptionHelloAPIError,
|
EncryptionPlaintextAPIError,
|
||||||
EntityInfo,
|
EntityInfo,
|
||||||
HomeassistantServiceCall,
|
HomeassistantServiceCall,
|
||||||
InvalidAuthAPIError,
|
InvalidAuthAPIError,
|
||||||
@@ -571,7 +571,7 @@ class ESPHomeManager:
|
|||||||
if isinstance(
|
if isinstance(
|
||||||
err,
|
err,
|
||||||
(
|
(
|
||||||
EncryptionHelloAPIError,
|
EncryptionPlaintextAPIError,
|
||||||
RequiresEncryptionAPIError,
|
RequiresEncryptionAPIError,
|
||||||
InvalidEncryptionKeyAPIError,
|
InvalidEncryptionKeyAPIError,
|
||||||
InvalidAuthAPIError,
|
InvalidAuthAPIError,
|
||||||
|
@@ -16,9 +16,9 @@
|
|||||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||||
"mqtt": ["esphome/discover/#"],
|
"mqtt": ["esphome/discover/#"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==29.8.0",
|
"aioesphomeapi==29.9.0",
|
||||||
"esphome-dashboard-api==1.2.3",
|
"esphome-dashboard-api==1.2.3",
|
||||||
"bleak-esphome==2.12.0"
|
"bleak-esphome==2.13.1"
|
||||||
],
|
],
|
||||||
"zeroconf": ["_esphomelib._tcp.local."]
|
"zeroconf": ["_esphomelib._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -152,7 +152,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
|||||||
super().__init__(coordinator, evo_device)
|
super().__init__(coordinator, evo_device)
|
||||||
self._evo_id = evo_device.id
|
self._evo_id = evo_device.id
|
||||||
|
|
||||||
if evo_device.model.startswith("VisionProWifi"):
|
if evo_device.id == evo_device.tcs.id:
|
||||||
# this system does not have a distinct ID for the zone
|
# this system does not have a distinct ID for the zone
|
||||||
self._attr_unique_id = f"{evo_device.id}z"
|
self._attr_unique_id = f"{evo_device.id}z"
|
||||||
else:
|
else:
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||||
"quality_scale": "legacy",
|
"quality_scale": "legacy",
|
||||||
"requirements": ["evohome-async==1.0.4"]
|
"requirements": ["evohome-async==1.0.5"]
|
||||||
}
|
}
|
||||||
|
@@ -14,9 +14,10 @@ from pyfibaro.fibaro_client import (
|
|||||||
)
|
)
|
||||||
from pyfibaro.fibaro_data_helper import read_rooms
|
from pyfibaro.fibaro_data_helper import read_rooms
|
||||||
from pyfibaro.fibaro_device import DeviceModel
|
from pyfibaro.fibaro_device import DeviceModel
|
||||||
|
from pyfibaro.fibaro_device_manager import FibaroDeviceManager
|
||||||
from pyfibaro.fibaro_info import InfoModel
|
from pyfibaro.fibaro_info import InfoModel
|
||||||
from pyfibaro.fibaro_scene import SceneModel
|
from pyfibaro.fibaro_scene import SceneModel
|
||||||
from pyfibaro.fibaro_state_resolver import FibaroEvent, FibaroStateResolver
|
from pyfibaro.fibaro_state_resolver import FibaroEvent
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform
|
||||||
@@ -81,8 +82,8 @@ class FibaroController:
|
|||||||
self._client = fibaro_client
|
self._client = fibaro_client
|
||||||
self._fibaro_info = info
|
self._fibaro_info = info
|
||||||
|
|
||||||
# Whether to import devices from plugins
|
# The fibaro device manager exposes higher level API to access fibaro devices
|
||||||
self._import_plugins = import_plugins
|
self._fibaro_device_manager = FibaroDeviceManager(fibaro_client, import_plugins)
|
||||||
# Mapping roomId to room object
|
# Mapping roomId to room object
|
||||||
self._room_map = read_rooms(fibaro_client)
|
self._room_map = read_rooms(fibaro_client)
|
||||||
self._device_map: dict[int, DeviceModel] # Mapping deviceId to device object
|
self._device_map: dict[int, DeviceModel] # Mapping deviceId to device object
|
||||||
@@ -91,79 +92,30 @@ class FibaroController:
|
|||||||
) # List of devices by entity platform
|
) # List of devices by entity platform
|
||||||
# All scenes
|
# All scenes
|
||||||
self._scenes = self._client.read_scenes()
|
self._scenes = self._client.read_scenes()
|
||||||
self._callbacks: dict[int, list[Any]] = {} # Update value callbacks by deviceId
|
|
||||||
# Event callbacks by device id
|
|
||||||
self._event_callbacks: dict[int, list[Callable[[FibaroEvent], None]]] = {}
|
|
||||||
# Unique serial number of the hub
|
# Unique serial number of the hub
|
||||||
self.hub_serial = info.serial_number
|
self.hub_serial = info.serial_number
|
||||||
# Device infos by fibaro device id
|
# Device infos by fibaro device id
|
||||||
self._device_infos: dict[int, DeviceInfo] = {}
|
self._device_infos: dict[int, DeviceInfo] = {}
|
||||||
self._read_devices()
|
self._read_devices()
|
||||||
|
|
||||||
def enable_state_handler(self) -> None:
|
def disconnect(self) -> None:
|
||||||
"""Start StateHandler thread for monitoring updates."""
|
"""Close push channel."""
|
||||||
self._client.register_update_handler(self._on_state_change)
|
self._fibaro_device_manager.close()
|
||||||
|
|
||||||
def disable_state_handler(self) -> None:
|
def register(
|
||||||
"""Stop StateHandler thread used for monitoring updates."""
|
self, device_id: int, callback: Callable[[DeviceModel], None]
|
||||||
self._client.unregister_update_handler()
|
) -> Callable[[], None]:
|
||||||
|
|
||||||
def _on_state_change(self, state: Any) -> None:
|
|
||||||
"""Handle change report received from the HomeCenter."""
|
|
||||||
callback_set = set()
|
|
||||||
for change in state.get("changes", []):
|
|
||||||
try:
|
|
||||||
dev_id = change.pop("id")
|
|
||||||
if dev_id not in self._device_map:
|
|
||||||
continue
|
|
||||||
device = self._device_map[dev_id]
|
|
||||||
for property_name, value in change.items():
|
|
||||||
if property_name == "log":
|
|
||||||
if value and value != "transfer OK":
|
|
||||||
_LOGGER.debug("LOG %s: %s", device.friendly_name, value)
|
|
||||||
continue
|
|
||||||
if property_name == "logTemp":
|
|
||||||
continue
|
|
||||||
if property_name in device.properties:
|
|
||||||
device.properties[property_name] = value
|
|
||||||
_LOGGER.debug(
|
|
||||||
"<- %s.%s = %s", device.ha_id, property_name, str(value)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("%s.%s not found", device.ha_id, property_name)
|
|
||||||
if dev_id in self._callbacks:
|
|
||||||
callback_set.add(dev_id)
|
|
||||||
except (ValueError, KeyError):
|
|
||||||
pass
|
|
||||||
for item in callback_set:
|
|
||||||
for callback in self._callbacks[item]:
|
|
||||||
callback()
|
|
||||||
|
|
||||||
resolver = FibaroStateResolver(state)
|
|
||||||
for event in resolver.get_events():
|
|
||||||
# event does not always have a fibaro id, therefore it is
|
|
||||||
# essential that we first check for relevant event type
|
|
||||||
if (
|
|
||||||
event.event_type.lower() == "centralsceneevent"
|
|
||||||
and event.fibaro_id in self._event_callbacks
|
|
||||||
):
|
|
||||||
for callback in self._event_callbacks[event.fibaro_id]:
|
|
||||||
callback(event)
|
|
||||||
|
|
||||||
def register(self, device_id: int, callback: Any) -> None:
|
|
||||||
"""Register device with a callback for updates."""
|
"""Register device with a callback for updates."""
|
||||||
device_callbacks = self._callbacks.setdefault(device_id, [])
|
return self._fibaro_device_manager.add_change_listener(device_id, callback)
|
||||||
device_callbacks.append(callback)
|
|
||||||
|
|
||||||
def register_event(
|
def register_event(
|
||||||
self, device_id: int, callback: Callable[[FibaroEvent], None]
|
self, device_id: int, callback: Callable[[FibaroEvent], None]
|
||||||
) -> None:
|
) -> Callable[[], None]:
|
||||||
"""Register device with a callback for central scene events.
|
"""Register device with a callback for central scene events.
|
||||||
|
|
||||||
The callback receives one parameter with the event.
|
The callback receives one parameter with the event.
|
||||||
"""
|
"""
|
||||||
device_callbacks = self._event_callbacks.setdefault(device_id, [])
|
return self._fibaro_device_manager.add_event_listener(device_id, callback)
|
||||||
device_callbacks.append(callback)
|
|
||||||
|
|
||||||
def get_children(self, device_id: int) -> list[DeviceModel]:
|
def get_children(self, device_id: int) -> list[DeviceModel]:
|
||||||
"""Get a list of child devices."""
|
"""Get a list of child devices."""
|
||||||
@@ -286,7 +238,7 @@ class FibaroController:
|
|||||||
|
|
||||||
def _read_devices(self) -> None:
|
def _read_devices(self) -> None:
|
||||||
"""Read and process the device list."""
|
"""Read and process the device list."""
|
||||||
devices = self._client.read_devices()
|
devices = self._fibaro_device_manager.get_devices()
|
||||||
self._device_map = {}
|
self._device_map = {}
|
||||||
last_climate_parent = None
|
last_climate_parent = None
|
||||||
last_endpoint = None
|
last_endpoint = None
|
||||||
@@ -301,8 +253,8 @@ class FibaroController:
|
|||||||
device.ha_id = (
|
device.ha_id = (
|
||||||
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
|
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
|
||||||
)
|
)
|
||||||
if device.enabled and (not device.is_plugin or self._import_plugins):
|
|
||||||
platform = self._map_device_to_platform(device)
|
platform = self._map_device_to_platform(device)
|
||||||
if platform is None:
|
if platform is None:
|
||||||
continue
|
continue
|
||||||
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
|
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
|
||||||
@@ -392,8 +344,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bo
|
|||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
controller.enable_state_handler()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -402,8 +352,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> b
|
|||||||
_LOGGER.debug("Shutting down Fibaro connection")
|
_LOGGER.debug("Shutting down Fibaro connection")
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
entry.runtime_data.disable_state_handler()
|
entry.runtime_data.disconnect()
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
@@ -36,9 +36,13 @@ class FibaroEntity(Entity):
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
self.controller.register(self.fibaro_device.fibaro_id, self._update_callback)
|
self.async_on_remove(
|
||||||
|
self.controller.register(
|
||||||
|
self.fibaro_device.fibaro_id, self._update_callback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _update_callback(self) -> None:
|
def _update_callback(self, fibaro_device: DeviceModel) -> None:
|
||||||
"""Update the state."""
|
"""Update the state."""
|
||||||
self.schedule_update_ha_state(True)
|
self.schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
@@ -60,11 +60,16 @@ class FibaroEventEntity(FibaroEntity, EventEntity):
|
|||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
# Register event callback
|
# Register event callback
|
||||||
self.controller.register_event(
|
self.async_on_remove(
|
||||||
self.fibaro_device.fibaro_id, self._event_callback
|
self.controller.register_event(
|
||||||
|
self.fibaro_device.fibaro_id, self._event_callback
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _event_callback(self, event: FibaroEvent) -> None:
|
def _event_callback(self, event: FibaroEvent) -> None:
|
||||||
if event.key_id == self._button:
|
if (
|
||||||
|
event.event_type.lower() == "centralsceneevent"
|
||||||
|
and event.key_id == self._button
|
||||||
|
):
|
||||||
self._trigger_event(event.key_event_type)
|
self._trigger_event(event.key_event_type)
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@@ -53,5 +53,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["flux_led"],
|
"loggers": ["flux_led"],
|
||||||
"requirements": ["flux-led==1.1.3"]
|
"requirements": ["flux-led==1.2.0"]
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["forecast-solar==4.0.0"]
|
"requirements": ["forecast-solar==4.1.0"]
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FritzBinarySensorEntityDescription(
|
class FritzBinarySensorEntityDescription(
|
||||||
|
@@ -31,6 +31,9 @@ from .entity import FritzDeviceBase
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Set a sane value to avoid too many updates
|
||||||
|
PARALLEL_UPDATES = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FritzButtonDescription(ButtonEntityDescription):
|
class FritzButtonDescription(ButtonEntityDescription):
|
||||||
|
@@ -22,6 +22,9 @@ from .entity import FritzDeviceBase
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@@ -18,6 +18,9 @@ from .entity import FritzBoxBaseEntity
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@@ -14,9 +14,7 @@ rules:
|
|||||||
docs-actions: done
|
docs-actions: done
|
||||||
docs-high-level-description: done
|
docs-high-level-description: done
|
||||||
docs-installation-instructions: done
|
docs-installation-instructions: done
|
||||||
docs-removal-instructions:
|
docs-removal-instructions: done
|
||||||
status: todo
|
|
||||||
comment: include the proper docs snippet
|
|
||||||
entity-event-setup: done
|
entity-event-setup: done
|
||||||
entity-unique-id: done
|
entity-unique-id: done
|
||||||
has-entity-name:
|
has-entity-name:
|
||||||
@@ -31,15 +29,11 @@ rules:
|
|||||||
action-exceptions: done
|
action-exceptions: done
|
||||||
config-entry-unloading: done
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters: done
|
docs-configuration-parameters: done
|
||||||
docs-installation-parameters:
|
docs-installation-parameters: done
|
||||||
status: todo
|
|
||||||
comment: add the proper configuration_basic block
|
|
||||||
entity-unavailable: done
|
entity-unavailable: done
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates:
|
parallel-updates: done
|
||||||
status: todo
|
|
||||||
comment: not set at the moment, we use a coordinator
|
|
||||||
reauthentication-flow: done
|
reauthentication-flow: done
|
||||||
test-coverage:
|
test-coverage:
|
||||||
status: todo
|
status: todo
|
||||||
@@ -50,7 +44,7 @@ rules:
|
|||||||
diagnostics: done
|
diagnostics: done
|
||||||
discovery-update-info: todo
|
discovery-update-info: todo
|
||||||
discovery: done
|
discovery: done
|
||||||
docs-data-update: todo
|
docs-data-update: done
|
||||||
docs-examples: done
|
docs-examples: done
|
||||||
docs-known-limitations:
|
docs-known-limitations:
|
||||||
status: exempt
|
status: exempt
|
||||||
|
@@ -32,6 +32,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime:
|
def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime:
|
||||||
"""Calculate uptime with deviation."""
|
"""Calculate uptime with deviation."""
|
||||||
|
@@ -38,6 +38,9 @@ from .entity import FritzBoxBaseEntity, FritzDeviceBase
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Set a sane value to avoid too many updates
|
||||||
|
PARALLEL_UPDATES = 5
|
||||||
|
|
||||||
|
|
||||||
async def _async_deflection_entities_list(
|
async def _async_deflection_entities_list(
|
||||||
avm_wrapper: AvmWrapper, device_friendly_name: str
|
avm_wrapper: AvmWrapper, device_friendly_name: str
|
||||||
|
@@ -20,6 +20,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Set a sane value to avoid too many updates
|
||||||
|
PARALLEL_UPDATES = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription):
|
class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription):
|
||||||
|
@@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
|||||||
key="battery",
|
key="battery",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
suitable=lambda device: device.battery_level is not None,
|
suitable=lambda device: device.battery_level is not None,
|
||||||
native_value=lambda device: device.battery_level,
|
native_value=lambda device: device.battery_level,
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250401.0"]
|
"requirements": ["home-assistant-frontend==20250404.0"]
|
||||||
}
|
}
|
||||||
|
@@ -79,9 +79,9 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"no_data": "No data",
|
"no_data": "No data",
|
||||||
"too_low": "Too low",
|
"too_low": "Too low",
|
||||||
"low": "Low",
|
"low": "[%key:common::state::low%]",
|
||||||
"perfect": "Perfect",
|
"perfect": "Perfect",
|
||||||
"high": "High",
|
"high": "[%key:common::state::high%]",
|
||||||
"too_high": "Too high"
|
"too_high": "Too high"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -90,9 +90,9 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -101,9 +101,9 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -112,9 +112,9 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -123,9 +123,9 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"loggers": ["googleapiclient"],
|
||||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
|
||||||
}
|
}
|
||||||
|
@@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||||
if user_input[CONF_LLM_HASS_API] == "none":
|
if user_input[CONF_LLM_HASS_API] == "none":
|
||||||
user_input.pop(CONF_LLM_HASS_API)
|
user_input.pop(CONF_LLM_HASS_API)
|
||||||
return self.async_create_entry(title="", data=user_input)
|
if not (
|
||||||
|
user_input.get(CONF_LLM_HASS_API, "none") != "none"
|
||||||
|
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||||
|
):
|
||||||
|
# Don't allow to save options that enable the Google Seearch tool with an Assist API
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
|
||||||
|
|
||||||
# Re-render the options again, now with the recommended options shown/hidden
|
# Re-render the options again, now with the recommended options shown/hidden
|
||||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||||
|
|
||||||
options = {
|
options = user_input
|
||||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
|
||||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
|
||||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
|
||||||
}
|
|
||||||
|
|
||||||
schema = await google_generative_ai_config_option_schema(
|
schema = await google_generative_ai_config_option_schema(
|
||||||
self.hass, options, self._genai_client
|
self.hass, options, self._genai_client
|
||||||
)
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||||
data_schema=vol.Schema(schema),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -301,7 +303,7 @@ async def google_generative_ai_config_option_schema(
|
|||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
description={"suggested_value": options.get(CONF_TEMPERATURE)},
|
description={"suggested_value": options.get(CONF_TEMPERATURE)},
|
||||||
default=RECOMMENDED_TEMPERATURE,
|
default=RECOMMENDED_TEMPERATURE,
|
||||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TOP_P,
|
CONF_TOP_P,
|
||||||
description={"suggested_value": options.get(CONF_TOP_P)},
|
description={"suggested_value": options.get(CONF_TOP_P)},
|
||||||
|
@@ -55,6 +55,10 @@ from .const import (
|
|||||||
# Max number of back and forth with the LLM to generate a response
|
# Max number of back and forth with the LLM to generate a response
|
||||||
MAX_TOOL_ITERATIONS = 10
|
MAX_TOOL_ITERATIONS = 10
|
||||||
|
|
||||||
|
ERROR_GETTING_RESPONSE = (
|
||||||
|
"Sorry, I had a problem getting a response from Google Generative AI."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -429,6 +433,12 @@ class GoogleGenerativeAIConversationEntity(
|
|||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
|
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
|
||||||
)
|
)
|
||||||
|
if not chat_response.candidates:
|
||||||
|
LOGGER.error(
|
||||||
|
"No candidates found in the response: %s",
|
||||||
|
chat_response,
|
||||||
|
)
|
||||||
|
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||||
|
|
||||||
except (
|
except (
|
||||||
APIError,
|
APIError,
|
||||||
@@ -452,9 +462,7 @@ class GoogleGenerativeAIConversationEntity(
|
|||||||
|
|
||||||
response_parts = chat_response.candidates[0].content.parts
|
response_parts = chat_response.candidates[0].content.parts
|
||||||
if not response_parts:
|
if not response_parts:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||||
"Sorry, I had a problem getting a response from Google Generative AI."
|
|
||||||
)
|
|
||||||
content = " ".join(
|
content = " ".join(
|
||||||
[part.text.strip() for part in response_parts if part.text]
|
[part.text.strip() for part in response_parts if part.text]
|
||||||
)
|
)
|
||||||
|
@@ -40,9 +40,13 @@
|
|||||||
"enable_google_search_tool": "Enable Google Search tool"
|
"enable_google_search_tool": "Enable Google Search tool"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||||
|
"enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
@@ -265,6 +265,11 @@
|
|||||||
"version_latest": {
|
"version_latest": {
|
||||||
"name": "Newest version"
|
"name": "Newest version"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"update": {
|
||||||
|
"name": "[%key:component::update::title%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
@@ -39,7 +39,7 @@ from .entity import (
|
|||||||
from .update_helper import update_addon, update_core
|
from .update_helper import update_addon, update_core
|
||||||
|
|
||||||
ENTITY_DESCRIPTION = UpdateEntityDescription(
|
ENTITY_DESCRIPTION = UpdateEntityDescription(
|
||||||
name="Update",
|
translation_key="update",
|
||||||
key=ATTR_VERSION_LATEST,
|
key=ATTR_VERSION_LATEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -182,6 +182,6 @@ async def websocket_update_addon(
|
|||||||
async def websocket_update_core(
|
async def websocket_update_core(
|
||||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Websocket handler to update an addon."""
|
"""Websocket handler to update Home Assistant Core."""
|
||||||
await update_core(hass, None, msg["backup"])
|
await update_core(hass, None, msg["backup"])
|
||||||
connection.send_result(msg[WS_ID])
|
connection.send_result(msg[WS_ID])
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
ATTR_PASSWORD = "password"
|
ATTR_PASSWORD = "password"
|
||||||
ATTR_USERNAME = "username"
|
ATTR_USERNAME = "username"
|
||||||
|
ATTR_DESTINATION_POSITION = "destination_position"
|
||||||
ATTR_QUEUE_IDS = "queue_ids"
|
ATTR_QUEUE_IDS = "queue_ids"
|
||||||
DOMAIN = "heos"
|
DOMAIN = "heos"
|
||||||
ENTRY_TITLE = "HEOS System"
|
ENTRY_TITLE = "HEOS System"
|
||||||
@@ -9,6 +10,7 @@ SERVICE_GET_QUEUE = "get_queue"
|
|||||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||||
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
||||||
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
||||||
|
SERVICE_MOVE_QUEUE_ITEM = "move_queue_item"
|
||||||
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
||||||
SERVICE_SIGN_IN = "sign_in"
|
SERVICE_SIGN_IN = "sign_in"
|
||||||
SERVICE_SIGN_OUT = "sign_out"
|
SERVICE_SIGN_OUT = "sign_out"
|
||||||
|
@@ -6,6 +6,9 @@
|
|||||||
"remove_from_queue": {
|
"remove_from_queue": {
|
||||||
"service": "mdi:playlist-remove"
|
"service": "mdi:playlist-remove"
|
||||||
},
|
},
|
||||||
|
"move_queue_item": {
|
||||||
|
"service": "mdi:playlist-edit"
|
||||||
|
},
|
||||||
"group_volume_set": {
|
"group_volume_set": {
|
||||||
"service": "mdi:volume-medium"
|
"service": "mdi:volume-medium"
|
||||||
},
|
},
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyheos"],
|
"loggers": ["pyheos"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyheos==1.0.4"],
|
"requirements": ["pyheos==1.0.5"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||||
|
@@ -71,6 +71,7 @@ BASE_SUPPORTED_FEATURES = (
|
|||||||
|
|
||||||
PLAY_STATE_TO_STATE = {
|
PLAY_STATE_TO_STATE = {
|
||||||
None: MediaPlayerState.IDLE,
|
None: MediaPlayerState.IDLE,
|
||||||
|
PlayState.UNKNOWN: MediaPlayerState.IDLE,
|
||||||
PlayState.PLAY: MediaPlayerState.PLAYING,
|
PlayState.PLAY: MediaPlayerState.PLAYING,
|
||||||
PlayState.STOP: MediaPlayerState.IDLE,
|
PlayState.STOP: MediaPlayerState.IDLE,
|
||||||
PlayState.PAUSE: MediaPlayerState.PAUSED,
|
PlayState.PAUSE: MediaPlayerState.PAUSED,
|
||||||
@@ -479,6 +480,13 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
|||||||
"""Remove items from the queue."""
|
"""Remove items from the queue."""
|
||||||
await self._player.remove_from_queue(queue_ids)
|
await self._player.remove_from_queue(queue_ids)
|
||||||
|
|
||||||
|
@catch_action_error("move queue item")
|
||||||
|
async def async_move_queue_item(
|
||||||
|
self, queue_ids: list[int], destination_position: int
|
||||||
|
) -> None:
|
||||||
|
"""Move items in the queue."""
|
||||||
|
await self._player.move_queue_item(queue_ids, destination_position)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if the device is available."""
|
"""Return True if the device is available."""
|
||||||
|
@@ -19,6 +19,7 @@ from homeassistant.helpers import (
|
|||||||
from homeassistant.helpers.typing import VolDictType, VolSchemaType
|
from homeassistant.helpers.typing import VolDictType, VolSchemaType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ATTR_DESTINATION_POSITION,
|
||||||
ATTR_PASSWORD,
|
ATTR_PASSWORD,
|
||||||
ATTR_QUEUE_IDS,
|
ATTR_QUEUE_IDS,
|
||||||
ATTR_USERNAME,
|
ATTR_USERNAME,
|
||||||
@@ -27,6 +28,7 @@ from .const import (
|
|||||||
SERVICE_GROUP_VOLUME_DOWN,
|
SERVICE_GROUP_VOLUME_DOWN,
|
||||||
SERVICE_GROUP_VOLUME_SET,
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
SERVICE_GROUP_VOLUME_UP,
|
SERVICE_GROUP_VOLUME_UP,
|
||||||
|
SERVICE_MOVE_QUEUE_ITEM,
|
||||||
SERVICE_REMOVE_FROM_QUEUE,
|
SERVICE_REMOVE_FROM_QUEUE,
|
||||||
SERVICE_SIGN_IN,
|
SERVICE_SIGN_IN,
|
||||||
SERVICE_SIGN_OUT,
|
SERVICE_SIGN_OUT,
|
||||||
@@ -87,6 +89,16 @@ REMOVE_FROM_QUEUE_SCHEMA: Final[VolDictType] = {
|
|||||||
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
|
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
|
||||||
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
|
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
|
||||||
}
|
}
|
||||||
|
MOVE_QEUEUE_ITEM_SCHEMA: Final[VolDictType] = {
|
||||||
|
vol.Required(ATTR_QUEUE_IDS): vol.All(
|
||||||
|
cv.ensure_list,
|
||||||
|
[vol.All(vol.Coerce(int), vol.Range(min=1, max=1000))],
|
||||||
|
vol.Unique(),
|
||||||
|
),
|
||||||
|
vol.Required(ATTR_DESTINATION_POSITION): vol.All(
|
||||||
|
vol.Coerce(int), vol.Range(min=1, max=1000)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
||||||
# Player queue services
|
# Player queue services
|
||||||
@@ -96,6 +108,9 @@ MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
|||||||
EntityServiceDescription(
|
EntityServiceDescription(
|
||||||
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
|
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
|
||||||
),
|
),
|
||||||
|
EntityServiceDescription(
|
||||||
|
SERVICE_MOVE_QUEUE_ITEM, "async_move_queue_item", MOVE_QEUEUE_ITEM_SCHEMA
|
||||||
|
),
|
||||||
# Group volume services
|
# Group volume services
|
||||||
EntityServiceDescription(
|
EntityServiceDescription(
|
||||||
SERVICE_GROUP_VOLUME_SET,
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
|
@@ -17,6 +17,26 @@ remove_from_queue:
|
|||||||
multiple: true
|
multiple: true
|
||||||
type: number
|
type: number
|
||||||
|
|
||||||
|
move_queue_item:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: heos
|
||||||
|
domain: media_player
|
||||||
|
fields:
|
||||||
|
queue_ids:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiple: true
|
||||||
|
type: number
|
||||||
|
destination_position:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 1000
|
||||||
|
step: 1
|
||||||
|
|
||||||
group_volume_set:
|
group_volume_set:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
@@ -100,6 +100,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"move_queue_item": {
|
||||||
|
"name": "Move queue item",
|
||||||
|
"description": "Move one or more items within the play queue.",
|
||||||
|
"fields": {
|
||||||
|
"queue_ids": {
|
||||||
|
"name": "Queue IDs",
|
||||||
|
"description": "The IDs (indexes) of the items in the queue to move."
|
||||||
|
},
|
||||||
|
"destination_position": {
|
||||||
|
"name": "Destination position",
|
||||||
|
"description": "The position index in the queue to move the items to."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"group_volume_down": {
|
"group_volume_down": {
|
||||||
"name": "Turn down group volume",
|
"name": "Turn down group volume",
|
||||||
"description": "Turns down the group volume."
|
"description": "Turns down the group volume."
|
||||||
|
@@ -34,9 +34,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_username": "Failed to sign into Hive. Your email address is not recognised.",
|
"invalid_username": "Failed to sign in to Hive. Your email address is not recognised.",
|
||||||
"invalid_password": "Failed to sign into Hive. Incorrect password, please try again.",
|
"invalid_password": "Failed to sign in to Hive. Incorrect password, please try again.",
|
||||||
"invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.",
|
"invalid_code": "Failed to sign in to Hive. Your two-factor authentication code was incorrect.",
|
||||||
"no_internet_available": "An Internet connection is required to connect to Hive.",
|
"no_internet_available": "An Internet connection is required to connect to Hive.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
|
@@ -5,37 +5,18 @@ from typing import cast
|
|||||||
|
|
||||||
from aiohomeconnect.model import EventKey, StatusKey
|
from aiohomeconnect.model import EventKey, StatusKey
|
||||||
|
|
||||||
from homeassistant.components.automation import automations_with_entity
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.components.script import scripts_with_entity
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.issue_registry import (
|
|
||||||
IssueSeverity,
|
|
||||||
async_create_issue,
|
|
||||||
async_delete_issue,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .common import setup_home_connect_entry
|
from .common import setup_home_connect_entry
|
||||||
from .const import (
|
from .const import REFRIGERATION_STATUS_DOOR_CLOSED, REFRIGERATION_STATUS_DOOR_OPEN
|
||||||
BSH_DOOR_STATE_CLOSED,
|
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||||
BSH_DOOR_STATE_LOCKED,
|
|
||||||
BSH_DOOR_STATE_OPEN,
|
|
||||||
DOMAIN,
|
|
||||||
REFRIGERATION_STATUS_DOOR_CLOSED,
|
|
||||||
REFRIGERATION_STATUS_DOOR_OPEN,
|
|
||||||
)
|
|
||||||
from .coordinator import (
|
|
||||||
HomeConnectApplianceData,
|
|
||||||
HomeConnectConfigEntry,
|
|
||||||
HomeConnectCoordinator,
|
|
||||||
)
|
|
||||||
from .entity import HomeConnectEntity
|
from .entity import HomeConnectEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -173,8 +154,6 @@ def _get_entities_for_appliance(
|
|||||||
for description in BINARY_SENSORS
|
for description in BINARY_SENSORS
|
||||||
if description.key in appliance.status
|
if description.key in appliance.status
|
||||||
)
|
)
|
||||||
if StatusKey.BSH_COMMON_DOOR_STATE in appliance.status:
|
|
||||||
entities.append(HomeConnectDoorBinarySensor(entry.runtime_data, appliance))
|
|
||||||
return entities
|
return entities
|
||||||
|
|
||||||
|
|
||||||
@@ -220,83 +199,3 @@ class HomeConnectConnectivityBinarySensor(HomeConnectEntity, BinarySensorEntity)
|
|||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return the availability."""
|
"""Return the availability."""
|
||||||
return self.coordinator.last_update_success
|
return self.coordinator.last_update_success
|
||||||
|
|
||||||
|
|
||||||
class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
|
||||||
"""Binary sensor for Home Connect Generic Door."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: HomeConnectCoordinator,
|
|
||||||
appliance: HomeConnectApplianceData,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
super().__init__(
|
|
||||||
coordinator,
|
|
||||||
appliance,
|
|
||||||
HomeConnectBinarySensorEntityDescription(
|
|
||||||
key=StatusKey.BSH_COMMON_DOOR_STATE,
|
|
||||||
device_class=BinarySensorDeviceClass.DOOR,
|
|
||||||
boolean_map={
|
|
||||||
BSH_DOOR_STATE_CLOSED: False,
|
|
||||||
BSH_DOOR_STATE_LOCKED: False,
|
|
||||||
BSH_DOOR_STATE_OPEN: True,
|
|
||||||
},
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
|
|
||||||
self._attr_name = f"{appliance.info.name} Door"
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Call when entity is added to hass."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
automations = automations_with_entity(self.hass, self.entity_id)
|
|
||||||
scripts = scripts_with_entity(self.hass, self.entity_id)
|
|
||||||
items = automations + scripts
|
|
||||||
if not items:
|
|
||||||
return
|
|
||||||
|
|
||||||
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
|
||||||
entity_automations = [
|
|
||||||
automation_entity
|
|
||||||
for automation_id in automations
|
|
||||||
if (automation_entity := entity_reg.async_get(automation_id))
|
|
||||||
]
|
|
||||||
entity_scripts = [
|
|
||||||
script_entity
|
|
||||||
for script_id in scripts
|
|
||||||
if (script_entity := entity_reg.async_get(script_id))
|
|
||||||
]
|
|
||||||
|
|
||||||
items_list = [
|
|
||||||
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
|
|
||||||
for item in entity_automations
|
|
||||||
] + [
|
|
||||||
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
|
|
||||||
for item in entity_scripts
|
|
||||||
]
|
|
||||||
|
|
||||||
async_create_issue(
|
|
||||||
self.hass,
|
|
||||||
DOMAIN,
|
|
||||||
f"deprecated_binary_common_door_sensor_{self.entity_id}",
|
|
||||||
breaks_in_ha_version="2025.5.0",
|
|
||||||
is_fixable=True,
|
|
||||||
is_persistent=True,
|
|
||||||
severity=IssueSeverity.WARNING,
|
|
||||||
translation_key="deprecated_binary_common_door_sensor",
|
|
||||||
translation_placeholders={
|
|
||||||
"entity": self.entity_id,
|
|
||||||
"items": "\n".join(items_list),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Call when entity will be removed from hass."""
|
|
||||||
await super().async_will_remove_from_hass()
|
|
||||||
async_delete_issue(
|
|
||||||
self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
|
|
||||||
)
|
|
||||||
|
@@ -74,6 +74,19 @@ class HomeConnectApplianceData:
|
|||||||
self.settings.update(other.settings)
|
self.settings.update(other.settings)
|
||||||
self.status.update(other.status)
|
self.status.update(other.status)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
|
||||||
|
"""Return empty data."""
|
||||||
|
return cls(
|
||||||
|
commands=set(),
|
||||||
|
events={},
|
||||||
|
info=appliance,
|
||||||
|
options={},
|
||||||
|
programs=[],
|
||||||
|
settings={},
|
||||||
|
status={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HomeConnectCoordinator(
|
class HomeConnectCoordinator(
|
||||||
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
||||||
@@ -362,15 +375,7 @@ class HomeConnectCoordinator(
|
|||||||
model=appliance.vib,
|
model=appliance.vib,
|
||||||
)
|
)
|
||||||
if appliance.ha_id not in self.data:
|
if appliance.ha_id not in self.data:
|
||||||
self.data[appliance.ha_id] = HomeConnectApplianceData(
|
self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
|
||||||
commands=set(),
|
|
||||||
events={},
|
|
||||||
info=appliance,
|
|
||||||
options={},
|
|
||||||
programs=[],
|
|
||||||
settings={},
|
|
||||||
status={},
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.data[appliance.ha_id].info.connected = appliance.connected
|
self.data[appliance.ha_id].info.connected = appliance.connected
|
||||||
old_appliances.remove(appliance.ha_id)
|
old_appliances.remove(appliance.ha_id)
|
||||||
@@ -406,6 +411,15 @@ class HomeConnectCoordinator(
|
|||||||
name=appliance.name,
|
name=appliance.name,
|
||||||
model=appliance.vib,
|
model=appliance.vib,
|
||||||
)
|
)
|
||||||
|
if not appliance.connected:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Appliance %s is not connected, skipping data fetch",
|
||||||
|
appliance.ha_id,
|
||||||
|
)
|
||||||
|
if appliance_data_to_update:
|
||||||
|
appliance_data_to_update.info.connected = False
|
||||||
|
return appliance_data_to_update
|
||||||
|
return HomeConnectApplianceData.empty(appliance)
|
||||||
try:
|
try:
|
||||||
settings = {
|
settings = {
|
||||||
setting.key: setting
|
setting.key: setting
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
"requirements": ["aiohomeconnect==0.16.3"],
|
"requirements": ["aiohomeconnect==0.17.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -132,17 +132,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deprecated_binary_common_door_sensor": {
|
|
||||||
"title": "Deprecated binary door sensor detected in some automations or scripts",
|
|
||||||
"fix_flow": {
|
|
||||||
"step": {
|
|
||||||
"confirm": {
|
|
||||||
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
|
|
||||||
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deprecated_command_actions": {
|
"deprecated_command_actions": {
|
||||||
"title": "The command related actions are deprecated in favor of the new buttons",
|
"title": "The command related actions are deprecated in favor of the new buttons",
|
||||||
"fix_flow": {
|
"fix_flow": {
|
||||||
@@ -487,9 +476,9 @@
|
|||||||
},
|
},
|
||||||
"warming_level": {
|
"warming_level": {
|
||||||
"options": {
|
"options": {
|
||||||
"cooking_oven_enum_type_warming_level_low": "Low",
|
"cooking_oven_enum_type_warming_level_low": "[%key:common::state::low%]",
|
||||||
"cooking_oven_enum_type_warming_level_medium": "Medium",
|
"cooking_oven_enum_type_warming_level_medium": "[%key:common::state::medium%]",
|
||||||
"cooking_oven_enum_type_warming_level_high": "High"
|
"cooking_oven_enum_type_warming_level_high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"washer_temperature": {
|
"washer_temperature": {
|
||||||
@@ -522,9 +511,9 @@
|
|||||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
|
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
|
||||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
|
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
|
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:common::state::low%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
|
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:common::state::medium%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "High"
|
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vario_perfect": {
|
"vario_perfect": {
|
||||||
@@ -1468,9 +1457,9 @@
|
|||||||
"warming_level": {
|
"warming_level": {
|
||||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_warming_level::name%]",
|
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_warming_level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"cooking_oven_enum_type_warming_level_low": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_low%]",
|
"cooking_oven_enum_type_warming_level_low": "[%key:common::state::low%]",
|
||||||
"cooking_oven_enum_type_warming_level_medium": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_medium%]",
|
"cooking_oven_enum_type_warming_level_medium": "[%key:common::state::medium%]",
|
||||||
"cooking_oven_enum_type_warming_level_high": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_high%]"
|
"cooking_oven_enum_type_warming_level_high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"washer_temperature": {
|
"washer_temperature": {
|
||||||
@@ -1505,9 +1494,9 @@
|
|||||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
|
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
|
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
|
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:common::state::low%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",
|
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:common::state::medium%]",
|
||||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_high%]"
|
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vario_perfect": {
|
"vario_perfect": {
|
||||||
|
@@ -71,7 +71,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
# Postpone loading the config entry if the device is missing
|
# Postpone loading the config entry if the device is missing
|
||||||
device_path = entry.data[DEVICE]
|
device_path = entry.data[DEVICE]
|
||||||
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="device_disconnected",
|
||||||
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||||
|
|
||||||
|
@@ -5,17 +5,21 @@ from __future__ import annotations
|
|||||||
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
|
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .config_flow import HomeAssistantSkyConnectConfigFlow
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .util import get_hardware_variant
|
from .util import get_hardware_variant
|
||||||
|
|
||||||
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
|
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
|
||||||
|
EXPECTED_ENTRY_VERSION = (
|
||||||
|
HomeAssistantSkyConnectConfigFlow.VERSION,
|
||||||
|
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||||
"""Return board info."""
|
"""Return board info."""
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
HardwareInfo(
|
HardwareInfo(
|
||||||
board=None,
|
board=None,
|
||||||
@@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
|||||||
url=DOCUMENTATION_URL,
|
url=DOCUMENTATION_URL,
|
||||||
)
|
)
|
||||||
for entry in entries
|
for entry in entries
|
||||||
|
# Ignore unmigrated config entries in the hardware page
|
||||||
|
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
|
||||||
]
|
]
|
||||||
|
@@ -195,5 +195,10 @@
|
|||||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"device_disconnected": {
|
||||||
|
"message": "The device is not plugged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"flow_title": "Homee {name} ({host})",
|
"flow_title": "homee {name} ({host})",
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
},
|
},
|
||||||
@@ -18,9 +18,9 @@
|
|||||||
"username": "[%key:common::config_flow::data::username%]"
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The IP address of your Homee.",
|
"host": "The IP address of your homee.",
|
||||||
"username": "The username for your Homee.",
|
"username": "The username for your homee.",
|
||||||
"password": "The password for your Homee."
|
"password": "The password for your homee."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"load_alarm": {
|
"load_alarm": {
|
||||||
"name": "Load",
|
"name": "Load",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Normal",
|
"off": "[%key:common::state::normal%]",
|
||||||
"on": "Overload"
|
"on": "Overload"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"connection_closed": {
|
"connection_closed": {
|
||||||
"message": "Could not connect to Homee while setting attribute."
|
"message": "Could not connect to homee while setting attribute."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -659,13 +659,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
|
|||||||
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
|
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
|
||||||
# Can be 0 - 2 (Off, Heat, Cool)
|
# Can be 0 - 2 (Off, Heat, Cool)
|
||||||
|
|
||||||
# If the HVAC is switched off, it must be idle
|
|
||||||
# This works around a bug in some devices (like Eve radiator valves) that
|
|
||||||
# return they are heating when they are not.
|
|
||||||
target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||||
if target == HeatingCoolingTargetValues.OFF:
|
|
||||||
return HVACAction.IDLE
|
|
||||||
|
|
||||||
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
||||||
current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
|
current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
|
||||||
|
|
||||||
@@ -679,6 +673,12 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
|
|||||||
):
|
):
|
||||||
return HVACAction.FAN
|
return HVACAction.FAN
|
||||||
|
|
||||||
|
# If the HVAC is switched off, it must be idle
|
||||||
|
# This works around a bug in some devices (like Eve radiator valves) that
|
||||||
|
# return they are heating when they are not.
|
||||||
|
if target == HeatingCoolingTargetValues.OFF:
|
||||||
|
return HVACAction.IDLE
|
||||||
|
|
||||||
return current_hass_value
|
return current_hass_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Please enter the credentials used to log into mytotalconnectcomfort.com.",
|
"description": "Please enter the credentials used to log in to mytotalconnectcomfort.com.",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
@@ -3,25 +3,34 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from aiohttp import hdrs
|
||||||
from aiohttp.web import Application, Request, StreamResponse, middleware
|
from aiohttp.web import Application, Request, StreamResponse, middleware
|
||||||
from aiohttp.web_exceptions import HTTPException
|
from aiohttp.web_exceptions import HTTPException
|
||||||
|
from multidict import CIMultiDict, istr
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
REFERRER_POLICY: Final[istr] = istr("Referrer-Policy")
|
||||||
|
X_CONTENT_TYPE_OPTIONS: Final[istr] = istr("X-Content-Type-Options")
|
||||||
|
X_FRAME_OPTIONS: Final[istr] = istr("X-Frame-Options")
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def setup_headers(app: Application, use_x_frame_options: bool) -> None:
|
def setup_headers(app: Application, use_x_frame_options: bool) -> None:
|
||||||
"""Create headers middleware for the app."""
|
"""Create headers middleware for the app."""
|
||||||
|
|
||||||
added_headers = {
|
added_headers = CIMultiDict(
|
||||||
"Referrer-Policy": "no-referrer",
|
{
|
||||||
"X-Content-Type-Options": "nosniff",
|
REFERRER_POLICY: "no-referrer",
|
||||||
"Server": "", # Empty server header, to prevent aiohttp of setting one.
|
X_CONTENT_TYPE_OPTIONS: "nosniff",
|
||||||
}
|
hdrs.SERVER: "", # Empty server header, to prevent aiohttp of setting one.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if use_x_frame_options:
|
if use_x_frame_options:
|
||||||
added_headers["X-Frame-Options"] = "SAMEORIGIN"
|
added_headers[X_FRAME_OPTIONS] = "SAMEORIGIN"
|
||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
async def headers_middleware(
|
async def headers_middleware(
|
||||||
|
@@ -88,7 +88,7 @@ from .utils import get_device_macs, non_verifying_requests_session
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
NOTIFY_SCHEMA = vol.Any(
|
NOTIFY_SCHEMA = vol.Any(
|
||||||
None,
|
None,
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"huawei-lte-api==1.10.0",
|
"huawei-lte-api==1.10.0",
|
||||||
"stringcase==1.2.0",
|
"stringcase==1.2.0",
|
||||||
"url-normalize==1.4.3"
|
"url-normalize==2.2.0"
|
||||||
],
|
],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
|
@@ -197,5 +197,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_effect_none": {
|
||||||
|
"title": "Light turned on with deprecated effect",
|
||||||
|
"description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ from homeassistant.components.light import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.util import color as color_util
|
from homeassistant.util import color as color_util
|
||||||
|
|
||||||
from ..bridge import HueBridge
|
from ..bridge import HueBridge
|
||||||
@@ -44,6 +45,9 @@ FALLBACK_MIN_KELVIN = 6500
|
|||||||
FALLBACK_MAX_KELVIN = 2000
|
FALLBACK_MAX_KELVIN = 2000
|
||||||
FALLBACK_KELVIN = 5800 # halfway
|
FALLBACK_KELVIN = 5800 # halfway
|
||||||
|
|
||||||
|
# HA 2025.4 replaced the deprecated effect "None" with HA default "off"
|
||||||
|
DEPRECATED_EFFECT_NONE = "None"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -233,6 +237,23 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
self._color_temp_active = color_temp is not None
|
self._color_temp_active = color_temp is not None
|
||||||
flash = kwargs.get(ATTR_FLASH)
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||||
|
if effect_str == DEPRECATED_EFFECT_NONE:
|
||||||
|
# deprecated effect "None" is now "off"
|
||||||
|
effect_str = EFFECT_OFF
|
||||||
|
async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_effect_none",
|
||||||
|
breaks_in_ha_version="2025.10.0",
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_effect_none",
|
||||||
|
)
|
||||||
|
self.logger.warning(
|
||||||
|
"Detected deprecated effect 'None' in %s, use 'off' instead. "
|
||||||
|
"This will stop working in HA 2025.10",
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
if effect_str == EFFECT_OFF:
|
if effect_str == EFFECT_OFF:
|
||||||
# ignore effect if set to "off" and we have no effect active
|
# ignore effect if set to "off" and we have no effect active
|
||||||
# the special effect "off" is only used to stop an active effect
|
# the special effect "off" is only used to stop an active effect
|
||||||
|
@@ -62,7 +62,7 @@
|
|||||||
"mode": {
|
"mode": {
|
||||||
"name": "Mode",
|
"name": "Mode",
|
||||||
"state": {
|
"state": {
|
||||||
"normal": "Normal",
|
"normal": "[%key:common::state::normal%]",
|
||||||
"home": "[%key:common::state::home%]",
|
"home": "[%key:common::state::home%]",
|
||||||
"away": "[%key:common::state::not_home%]",
|
"away": "[%key:common::state::not_home%]",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user