mirror of
https://github.com/home-assistant/core.git
synced 2026-03-03 14:26:59 +01:00
Compare commits
34 Commits
epenet/202
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1e95c483d | ||
|
|
9cb6e02c5f | ||
|
|
2c75e3289a | ||
|
|
348012a6b8 | ||
|
|
e0db00e089 | ||
|
|
b2280198d9 | ||
|
|
9cc4a3e427 | ||
|
|
f94a075641 | ||
|
|
f1856e6ef6 | ||
|
|
ed35bafa6c | ||
|
|
66e16d728b | ||
|
|
a806efa7e2 | ||
|
|
ad4b4bd221 | ||
|
|
c9c9a149b6 | ||
|
|
0f9fdfe2de | ||
|
|
a76b63912d | ||
|
|
bc03e13d38 | ||
|
|
450aa9757d | ||
|
|
158389a4f2 | ||
|
|
95e89d5ef1 | ||
|
|
e107b8e5cd | ||
|
|
f875b43ede | ||
|
|
6242ef78c4 | ||
|
|
3c342c0768 | ||
|
|
5dba5fc79d | ||
|
|
713b7cf36d | ||
|
|
cb016b014b | ||
|
|
afb4523f63 | ||
|
|
05ad4986ac | ||
|
|
42dbd5f98f | ||
|
|
f58a514ce7 | ||
|
|
8fb384a5e1 | ||
|
|
c24302b5ce | ||
|
|
999ad9b642 |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -182,7 +182,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -544,7 +544,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -978,7 +978,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1387,7 +1387,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1558,7 +1558,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1587,7 +1587,7 @@ jobs:
|
||||
&& needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
||||
10
.github/workflows/wheels.yml
vendored
10
.github/workflows/wheels.yml
vendored
@@ -124,12 +124,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -175,17 +175,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ from .const import (
|
||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||
)
|
||||
from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError
|
||||
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
@@ -433,32 +433,56 @@ def _init_blocking_io_modules_in_executor() -> None:
|
||||
is_docker_env()
|
||||
|
||||
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
"""Load the registries and modules that will do blocking I/O."""
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||
"""Load the registries and modules that will do blocking I/O.
|
||||
|
||||
Return whether loading succeeded.
|
||||
"""
|
||||
if DATA_REGISTRIES_LOADED in hass.data:
|
||||
return
|
||||
return True
|
||||
|
||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||
entity.async_setup(hass)
|
||||
frame.async_setup(hass)
|
||||
template.async_setup(hass)
|
||||
translation.async_setup(hass)
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass)),
|
||||
create_eager_task(category_registry.async_load(hass)),
|
||||
create_eager_task(device_registry.async_load(hass)),
|
||||
create_eager_task(entity_registry.async_load(hass)),
|
||||
create_eager_task(floor_registry.async_load(hass)),
|
||||
create_eager_task(issue_registry.async_load(hass)),
|
||||
create_eager_task(label_registry.async_load(hass)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
recovery = hass.config.recovery_mode
|
||||
try:
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(category_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(device_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(label_registry.async_load(hass, load_empty=recovery)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
except UnsupportedStorageVersionError as err:
|
||||
# If we're already in recovery mode, we don't want to handle the exception
|
||||
# and activate recovery mode again, as that would lead to an infinite loop.
|
||||
if recovery:
|
||||
raise
|
||||
|
||||
_LOGGER.error(
|
||||
"Storage file %s was created by a newer version of Home Assistant"
|
||||
" (storage version %s > %s); activating recovery mode; on-disk data"
|
||||
" is preserved; upgrade Home Assistant or restore from a backup",
|
||||
err.storage_key,
|
||||
err.found_version,
|
||||
err.max_supported_version,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_from_config_dict(
|
||||
@@ -475,7 +499,9 @@ async def async_from_config_dict(
|
||||
# Prime custom component cache early so we know if registry entries are tied
|
||||
# to a custom integration
|
||||
await loader.async_get_custom_components(hass)
|
||||
await async_load_base_functionality(hass)
|
||||
|
||||
if not await async_load_base_functionality(hass):
|
||||
return None
|
||||
|
||||
# Set up core.
|
||||
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
||||
|
||||
5
homeassistant/brands/ubisys.json
Normal file
5
homeassistant/brands/ubisys.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "ubisys",
|
||||
"name": "Ubisys",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -44,7 +44,7 @@ def make_entity_state_trigger_required_features(
|
||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_to_states = {to_state}
|
||||
_required_features = required_features
|
||||
|
||||
|
||||
@@ -29,12 +29,17 @@ class StoredBackupData(TypedDict):
|
||||
class _BackupStore(Store[StoredBackupData]):
|
||||
"""Class to help storing backup data."""
|
||||
|
||||
# Maximum version we support reading for forward compatibility.
|
||||
# This allows reading data written by a newer HA version after downgrade.
|
||||
_MAX_READABLE_VERSION = 2
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize storage class."""
|
||||
super().__init__(
|
||||
hass,
|
||||
STORAGE_VERSION,
|
||||
STORAGE_KEY,
|
||||
max_readable_version=self._MAX_READABLE_VERSION,
|
||||
minor_version=STORAGE_VERSION_MINOR,
|
||||
)
|
||||
|
||||
@@ -86,8 +91,8 @@ class _BackupStore(Store[StoredBackupData]):
|
||||
# data["config"]["schedule"]["state"] will be removed. The bump to 2 is
|
||||
# planned to happen after a 6 month quiet period with no minor version
|
||||
# changes.
|
||||
# Reject if major version is higher than 2.
|
||||
if old_major_version > 2:
|
||||
# Reject if major version is higher than _MAX_READABLE_VERSION.
|
||||
if old_major_version > self._MAX_READABLE_VERSION:
|
||||
raise NotImplementedError
|
||||
return data
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase):
|
||||
"""Class for binary sensor on/off triggers."""
|
||||
|
||||
_device_class: BinarySensorDeviceClass | None
|
||||
_domain: str = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
|
||||
@@ -14,7 +14,7 @@ from . import DOMAIN
|
||||
class ButtonPressedTrigger(EntityTriggerBase):
|
||||
"""Trigger for button entity presses."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
"early_update": {
|
||||
"default": "mdi:update"
|
||||
},
|
||||
"equalizer": {
|
||||
"default": "mdi:equalizer",
|
||||
"state": {
|
||||
"off": "mdi:equalizer-outline"
|
||||
}
|
||||
},
|
||||
"pre_amp": {
|
||||
"default": "mdi:volume-high",
|
||||
"state": {
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
"early_update": {
|
||||
"name": "Early update"
|
||||
},
|
||||
"equalizer": {
|
||||
"name": "Equalizer"
|
||||
},
|
||||
"pre_amp": {
|
||||
"name": "Pre-Amp"
|
||||
},
|
||||
|
||||
@@ -33,6 +33,13 @@ def room_correction_enabled(client: StreamMagicClient) -> bool:
|
||||
return client.audio.tilt_eq.enabled
|
||||
|
||||
|
||||
def equalizer_enabled(client: StreamMagicClient) -> bool:
|
||||
"""Check if equalizer is enabled."""
|
||||
if TYPE_CHECKING:
|
||||
assert client.audio.user_eq is not None
|
||||
return client.audio.user_eq.enabled
|
||||
|
||||
|
||||
CONTROL_ENTITIES: tuple[CambridgeAudioSwitchEntityDescription, ...] = (
|
||||
CambridgeAudioSwitchEntityDescription(
|
||||
key="pre_amp",
|
||||
@@ -56,6 +63,14 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSwitchEntityDescription, ...] = (
|
||||
value_fn=room_correction_enabled,
|
||||
set_value_fn=lambda client, value: client.set_room_correction_mode(value),
|
||||
),
|
||||
CambridgeAudioSwitchEntityDescription(
|
||||
key="equalizer",
|
||||
translation_key="equalizer",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
load_fn=lambda client: client.audio.user_eq is not None,
|
||||
value_fn=equalizer_enabled,
|
||||
set_value_fn=lambda client, value: client.set_equalizer_mode(value),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
|
||||
class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
_schema = HVAC_MODE_CHANGED_TRIGGER_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
|
||||
@@ -48,6 +48,8 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
vol.Optional("conversation_id"): vol.Any(str, None),
|
||||
vol.Optional("language"): str,
|
||||
vol.Optional("agent_id"): agent_id_validator,
|
||||
vol.Optional("device_id"): vol.Any(str, None),
|
||||
vol.Optional("satellite_id"): vol.Any(str, None),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -64,6 +66,8 @@ async def websocket_process(
|
||||
context=connection.context(msg),
|
||||
language=msg.get("language"),
|
||||
agent_id=msg.get("agent_id"),
|
||||
device_id=msg.get("device_id"),
|
||||
satellite_id=msg.get("satellite_id"),
|
||||
)
|
||||
connection.send_result(msg["id"], result.as_dict())
|
||||
|
||||
@@ -248,6 +252,8 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
vol.Optional("conversation_id"): str,
|
||||
vol.Optional("language"): str,
|
||||
vol.Optional("agent_id"): agent_id_validator,
|
||||
vol.Optional("device_id"): vol.Any(str, None),
|
||||
vol.Optional("satellite_id"): vol.Any(str, None),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -262,6 +268,8 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
context=self.context(request),
|
||||
language=data.get("language"),
|
||||
agent_id=data.get("agent_id"),
|
||||
device_id=data.get("device_id"),
|
||||
satellite_id=data.get("satellite_id"),
|
||||
)
|
||||
|
||||
return self.json(result.as_dict())
|
||||
|
||||
@@ -112,11 +112,12 @@ def _zone_is_configured(zone: DaikinZone) -> bool:
|
||||
|
||||
def _zone_temperature_lists(device: Appliance) -> tuple[list[str], list[str]]:
|
||||
"""Return the decoded zone temperature lists."""
|
||||
try:
|
||||
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
|
||||
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
|
||||
except AttributeError, KeyError:
|
||||
values = device.values
|
||||
if DAIKIN_ZONE_TEMP_HEAT not in values or DAIKIN_ZONE_TEMP_COOL not in values:
|
||||
return ([], [])
|
||||
|
||||
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
|
||||
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
|
||||
return (list(heating or []), list(cooling or []))
|
||||
|
||||
|
||||
|
||||
@@ -2,14 +2,39 @@
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EafmConfigEntry, EafmCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
def _fix_device_registry_identifiers(
|
||||
hass: HomeAssistant, entry: EafmConfigEntry
|
||||
) -> None:
|
||||
"""Fix invalid identifiers in device registry.
|
||||
|
||||
Added in 2026.4, can be removed in 2026.10 or later.
|
||||
"""
|
||||
device_registry = dr.async_get(hass)
|
||||
for device_entry in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
old_identifier = (DOMAIN, "measure-id", entry.data["station"])
|
||||
if old_identifier not in device_entry.identifiers: # type: ignore[comparison-overlap]
|
||||
continue
|
||||
new_identifiers = device_entry.identifiers.copy()
|
||||
new_identifiers.discard(old_identifier) # type: ignore[arg-type]
|
||||
new_identifiers.add((DOMAIN, entry.data["station"]))
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, new_identifiers=new_identifiers
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: EafmConfigEntry) -> bool:
|
||||
"""Set up flood monitoring sensors for this config entry."""
|
||||
_fix_device_registry_identifiers(hass, entry)
|
||||
coordinator = EafmCoordinator(hass, entry=entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
@@ -94,11 +94,11 @@ class Measurement(CoordinatorEntity, SensorEntity):
|
||||
return self.coordinator.data["measures"][self.key]["parameterName"]
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, "measure-id", self.station_id)},
|
||||
identifiers={(DOMAIN, self.station_id)},
|
||||
manufacturer="https://environment.data.gov.uk/",
|
||||
model=self.parameter_name,
|
||||
name=f"{self.station_name} {self.parameter_name} {self.qualifier}",
|
||||
|
||||
@@ -189,6 +189,7 @@ async def platform_async_setup_entry(
|
||||
info_type: type[_InfoT],
|
||||
entity_type: type[_EntityT],
|
||||
state_type: type[_StateT],
|
||||
info_filter: Callable[[_InfoT], bool] | None = None,
|
||||
) -> None:
|
||||
"""Set up an esphome platform.
|
||||
|
||||
@@ -208,10 +209,22 @@ async def platform_async_setup_entry(
|
||||
entity_type,
|
||||
state_type,
|
||||
)
|
||||
|
||||
if info_filter is not None:
|
||||
|
||||
def on_filtered_update(infos: list[EntityInfo]) -> None:
|
||||
on_static_info_update(
|
||||
[info for info in infos if info_filter(cast(_InfoT, info))]
|
||||
)
|
||||
|
||||
info_callback = on_filtered_update
|
||||
else:
|
||||
info_callback = on_static_info_update
|
||||
|
||||
entry_data.cleanup_callbacks.append(
|
||||
entry_data.async_register_static_info_callback(
|
||||
info_type,
|
||||
on_static_info_update,
|
||||
info_callback,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from aioesphomeapi import (
|
||||
Event,
|
||||
EventInfo,
|
||||
FanInfo,
|
||||
InfraredInfo,
|
||||
LightInfo,
|
||||
LockInfo,
|
||||
MediaPlayerInfo,
|
||||
@@ -85,6 +86,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
||||
DateTimeInfo: Platform.DATETIME,
|
||||
EventInfo: Platform.EVENT,
|
||||
FanInfo: Platform.FAN,
|
||||
InfraredInfo: Platform.INFRARED,
|
||||
LightInfo: Platform.LIGHT,
|
||||
LockInfo: Platform.LOCK,
|
||||
MediaPlayerInfo: Platform.MEDIA_PLAYER,
|
||||
|
||||
59
homeassistant/components/esphome/infrared.py
Normal file
59
homeassistant/components/esphome/infrared.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Infrared platform for ESPHome."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from aioesphomeapi import EntityState, InfraredCapability, InfraredInfo
|
||||
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .entity import (
|
||||
EsphomeEntity,
|
||||
convert_api_error_ha_error,
|
||||
platform_async_setup_entry,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState], InfraredEntity):
|
||||
"""ESPHome infrared entity using native API."""
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
"""Call when device updates or entry data changes."""
|
||||
super()._on_device_update()
|
||||
if self._entry_data.available:
|
||||
# Infrared entities should go available as soon as the device comes online
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_api_error_ha_error
|
||||
async def async_send_command(self, command: InfraredCommand) -> None:
|
||||
"""Send an IR command."""
|
||||
timings = [
|
||||
interval
|
||||
for timing in command.get_raw_timings()
|
||||
for interval in (timing.high_us, -timing.low_us)
|
||||
]
|
||||
_LOGGER.debug("Sending command: %s", timings)
|
||||
|
||||
self._client.infrared_rf_transmit_raw_timings(
|
||||
self._static_info.key,
|
||||
carrier_frequency=command.modulation,
|
||||
timings=timings,
|
||||
device_id=self._static_info.device_id,
|
||||
)
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
info_type=InfraredInfo,
|
||||
entity_type=EsphomeInfraredEntity,
|
||||
state_type=EntityState,
|
||||
info_filter=lambda info: bool(info.capabilities & InfraredCapability.TRANSMITTER),
|
||||
)
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260226.0"]
|
||||
"requirements": ["home-assistant-frontend==20260302.0"]
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
"connectable": false,
|
||||
"local_name": "GVH5110*"
|
||||
},
|
||||
{
|
||||
"connectable": false,
|
||||
"local_name": "GV5140*"
|
||||
},
|
||||
{
|
||||
"connectable": false,
|
||||
"manufacturer_id": 1,
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfTemperature,
|
||||
@@ -72,6 +73,12 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
(DeviceClass.CO2, Units.CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
|
||||
key=f"{DeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.15.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2026.2.25.165736"
|
||||
"knx-frontend==2026.3.2.183756"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ def _convert_uint8_to_percentage(value: Any) -> float:
|
||||
class BrightnessChangedTrigger(EntityNumericalStateAttributeChangedTriggerBase):
|
||||
"""Trigger for brightness changed."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
_attribute = ATTR_BRIGHTNESS
|
||||
|
||||
_converter = staticmethod(_convert_uint8_to_percentage)
|
||||
@@ -34,7 +34,7 @@ class BrightnessCrossedThresholdTrigger(
|
||||
):
|
||||
"""Trigger for brightness crossed threshold."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
_attribute = ATTR_BRIGHTNESS
|
||||
_converter = staticmethod(_convert_uint8_to_percentage)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pylutron_caseta"],
|
||||
"requirements": ["pylutron-caseta==0.26.0"],
|
||||
"requirements": ["pylutron-caseta==0.27.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"properties": {
|
||||
|
||||
@@ -14,7 +14,6 @@ from chip.clusters.Types import NullValue
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from .const import (
|
||||
CLEAR_ALL_INDEX,
|
||||
CRED_TYPE_FACE,
|
||||
CRED_TYPE_FINGER_VEIN,
|
||||
CRED_TYPE_FINGERPRINT,
|
||||
@@ -222,42 +221,6 @@ def _format_user_response(user_data: Any) -> LockUserData | None:
|
||||
# --- Credential management helpers ---
|
||||
|
||||
|
||||
async def _clear_user_credentials(
|
||||
matter_client: MatterClient,
|
||||
node_id: int,
|
||||
endpoint_id: int,
|
||||
user_index: int,
|
||||
) -> None:
|
||||
"""Clear all credentials for a specific user.
|
||||
|
||||
Fetches the user to get credential list, then clears each credential.
|
||||
"""
|
||||
get_user_response = await matter_client.send_device_command(
|
||||
node_id=node_id,
|
||||
endpoint_id=endpoint_id,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=user_index),
|
||||
)
|
||||
|
||||
creds = _get_attr(get_user_response, "credentials")
|
||||
if not creds:
|
||||
return
|
||||
|
||||
for cred in creds:
|
||||
cred_type = _get_attr(cred, "credentialType")
|
||||
cred_index = _get_attr(cred, "credentialIndex")
|
||||
await matter_client.send_device_command(
|
||||
node_id=node_id,
|
||||
endpoint_id=endpoint_id,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=cred_type,
|
||||
credentialIndex=cred_index,
|
||||
),
|
||||
),
|
||||
timed_request_timeout_ms=LOCK_TIMED_REQUEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
|
||||
class LockEndpointNotFoundError(HomeAssistantError):
|
||||
"""Lock endpoint not found on node."""
|
||||
|
||||
@@ -557,33 +520,16 @@ async def clear_lock_user(
|
||||
node: MatterNode,
|
||||
user_index: int,
|
||||
) -> None:
|
||||
"""Clear a user from the lock, cleaning up credentials first.
|
||||
"""Clear a user from the lock.
|
||||
|
||||
Per the Matter spec, ClearUser also clears all associated credentials
|
||||
and schedules for the user.
|
||||
Use index 0xFFFE (CLEAR_ALL_INDEX) to clear all users.
|
||||
Raises HomeAssistantError on failure.
|
||||
"""
|
||||
lock_endpoint = _get_lock_endpoint_or_raise(node)
|
||||
_ensure_usr_support(lock_endpoint)
|
||||
|
||||
if user_index == CLEAR_ALL_INDEX:
|
||||
# Clear all: clear all credentials first, then all users
|
||||
await matter_client.send_device_command(
|
||||
node_id=node.node_id,
|
||||
endpoint_id=lock_endpoint.endpoint_id,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=None,
|
||||
),
|
||||
timed_request_timeout_ms=LOCK_TIMED_REQUEST_TIMEOUT_MS,
|
||||
)
|
||||
else:
|
||||
# Clear credentials for this specific user before deleting them
|
||||
await _clear_user_credentials(
|
||||
matter_client,
|
||||
node.node_id,
|
||||
lock_endpoint.endpoint_id,
|
||||
user_index,
|
||||
)
|
||||
|
||||
await matter_client.send_device_command(
|
||||
node_id=node.node_id,
|
||||
endpoint_id=lock_endpoint.endpoint_id,
|
||||
|
||||
@@ -18,20 +18,17 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
METOFFICE_COORDINATES,
|
||||
METOFFICE_DAILY_COORDINATOR,
|
||||
METOFFICE_HOURLY_COORDINATOR,
|
||||
METOFFICE_NAME,
|
||||
METOFFICE_TWICE_DAILY_COORDINATOR,
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
MetOfficeConfigEntry,
|
||||
MetOfficeRuntimeData,
|
||||
MetOfficeUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import MetOfficeUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MetOfficeConfigEntry) -> bool:
|
||||
"""Set up a Met Office entry."""
|
||||
|
||||
latitude: float = entry.data[CONF_LATITUDE]
|
||||
@@ -39,8 +36,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
api_key: str = entry.data[CONF_API_KEY]
|
||||
site_name: str = entry.data[CONF_NAME]
|
||||
|
||||
coordinates = f"{latitude}_{longitude}"
|
||||
|
||||
connection = Manager(api_key=api_key)
|
||||
|
||||
metoffice_hourly_coordinator = MetOfficeUpdateCoordinator(
|
||||
@@ -73,21 +68,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
frequency="twice-daily",
|
||||
)
|
||||
|
||||
metoffice_hass_data = hass.data.setdefault(DOMAIN, {})
|
||||
metoffice_hass_data[entry.entry_id] = {
|
||||
METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator,
|
||||
METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator,
|
||||
METOFFICE_TWICE_DAILY_COORDINATOR: metoffice_twice_daily_coordinator,
|
||||
METOFFICE_NAME: site_name,
|
||||
METOFFICE_COORDINATES: coordinates,
|
||||
}
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await asyncio.gather(
|
||||
metoffice_hourly_coordinator.async_config_entry_first_refresh(),
|
||||
metoffice_daily_coordinator.async_config_entry_first_refresh(),
|
||||
)
|
||||
|
||||
entry.runtime_data = MetOfficeRuntimeData(
|
||||
coordinates=f"{latitude}_{longitude}",
|
||||
hourly_coordinator=metoffice_hourly_coordinator,
|
||||
daily_coordinator=metoffice_daily_coordinator,
|
||||
twice_daily_coordinator=metoffice_twice_daily_coordinator,
|
||||
name=site_name,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
@@ -95,12 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def get_device_info(coordinates: str, name: str) -> DeviceInfo:
|
||||
|
||||
@@ -38,13 +38,6 @@ ATTRIBUTION = "Data provided by the Met Office"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
METOFFICE_COORDINATES = "metoffice_coordinates"
|
||||
METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator"
|
||||
METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator"
|
||||
METOFFICE_TWICE_DAILY_COORDINATOR = "metoffice_twice_daily_coordinator"
|
||||
METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions"
|
||||
METOFFICE_NAME = "metoffice_name"
|
||||
|
||||
CONDITION_CLASSES: dict[str, list[int]] = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: [0],
|
||||
ATTR_CONDITION_CLOUDY: [7, 8],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
@@ -22,6 +23,19 @@ from .const import DEFAULT_SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MetOfficeConfigEntry = ConfigEntry[MetOfficeRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetOfficeRuntimeData:
|
||||
"""Met Office config entry."""
|
||||
|
||||
coordinates: str
|
||||
hourly_coordinator: MetOfficeUpdateCoordinator
|
||||
daily_coordinator: MetOfficeUpdateCoordinator
|
||||
twice_daily_coordinator: MetOfficeUpdateCoordinator
|
||||
name: str
|
||||
|
||||
|
||||
class MetOfficeUpdateCoordinator(TimestampDataUpdateCoordinator[Forecast]):
|
||||
"""Coordinator for Met Office forecast data."""
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
@@ -30,15 +29,12 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import get_device_info
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONDITION_MAP,
|
||||
DOMAIN,
|
||||
METOFFICE_COORDINATES,
|
||||
METOFFICE_HOURLY_COORDINATOR,
|
||||
METOFFICE_NAME,
|
||||
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN
|
||||
from .coordinator import (
|
||||
MetOfficeConfigEntry,
|
||||
MetOfficeRuntimeData,
|
||||
MetOfficeUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import MetOfficeUpdateCoordinator
|
||||
from .helpers import get_attribute
|
||||
|
||||
ATTR_LAST_UPDATE = "last_update"
|
||||
@@ -172,19 +168,19 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MetOfficeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Met Office weather sensor platform."""
|
||||
entity_registry = er.async_get(hass)
|
||||
hass_data = hass.data[DOMAIN][entry.entry_id]
|
||||
hass_data = entry.runtime_data
|
||||
|
||||
# Remove daily entities from legacy config entries
|
||||
for description in SENSOR_TYPES:
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{description.key}_{hass_data[METOFFICE_COORDINATES]}_daily",
|
||||
f"{description.key}_{hass_data.coordinates}_daily",
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
@@ -192,20 +188,20 @@ async def async_setup_entry(
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"visibility_distance_{hass_data[METOFFICE_COORDINATES]}_daily",
|
||||
f"visibility_distance_{hass_data.coordinates}_daily",
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"visibility_distance_{hass_data[METOFFICE_COORDINATES]}",
|
||||
f"visibility_distance_{hass_data.coordinates}",
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
MetOfficeCurrentSensor(
|
||||
hass_data[METOFFICE_HOURLY_COORDINATOR],
|
||||
hass_data.hourly_coordinator,
|
||||
hass_data,
|
||||
description,
|
||||
)
|
||||
@@ -228,7 +224,7 @@ class MetOfficeCurrentSensor(
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MetOfficeUpdateCoordinator,
|
||||
hass_data: dict[str, Any],
|
||||
hass_data: MetOfficeRuntimeData,
|
||||
description: MetOfficeSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
@@ -237,9 +233,9 @@ class MetOfficeCurrentSensor(
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_device_info = get_device_info(
|
||||
coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME]
|
||||
coordinates=hass_data.coordinates, name=hass_data.name
|
||||
)
|
||||
self._attr_unique_id = f"{description.key}_{hass_data[METOFFICE_COORDINATES]}"
|
||||
self._attr_unique_id = f"{description.key}_{hass_data.coordinates}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
|
||||
@@ -23,7 +23,6 @@ from homeassistant.components.weather import (
|
||||
Forecast as WeatherForecast,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfLength,
|
||||
UnitOfPressure,
|
||||
@@ -42,40 +41,39 @@ from .const import (
|
||||
DAY_FORECAST_ATTRIBUTE_MAP,
|
||||
DOMAIN,
|
||||
HOURLY_FORECAST_ATTRIBUTE_MAP,
|
||||
METOFFICE_COORDINATES,
|
||||
METOFFICE_DAILY_COORDINATOR,
|
||||
METOFFICE_HOURLY_COORDINATOR,
|
||||
METOFFICE_NAME,
|
||||
METOFFICE_TWICE_DAILY_COORDINATOR,
|
||||
NIGHT_FORECAST_ATTRIBUTE_MAP,
|
||||
)
|
||||
from .coordinator import MetOfficeUpdateCoordinator
|
||||
from .coordinator import (
|
||||
MetOfficeConfigEntry,
|
||||
MetOfficeRuntimeData,
|
||||
MetOfficeUpdateCoordinator,
|
||||
)
|
||||
from .helpers import get_attribute
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MetOfficeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Met Office weather sensor platform."""
|
||||
entity_registry = er.async_get(hass)
|
||||
hass_data = hass.data[DOMAIN][entry.entry_id]
|
||||
hass_data = entry.runtime_data
|
||||
|
||||
# Remove daily entity from legacy config entries
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
WEATHER_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{hass_data[METOFFICE_COORDINATES]}_daily",
|
||||
f"{hass_data.coordinates}_daily",
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
MetOfficeWeather(
|
||||
hass_data[METOFFICE_DAILY_COORDINATOR],
|
||||
hass_data[METOFFICE_HOURLY_COORDINATOR],
|
||||
hass_data[METOFFICE_TWICE_DAILY_COORDINATOR],
|
||||
hass_data.daily_coordinator,
|
||||
hass_data.hourly_coordinator,
|
||||
hass_data.twice_daily_coordinator,
|
||||
hass_data,
|
||||
)
|
||||
],
|
||||
@@ -178,7 +176,7 @@ class MetOfficeWeather(
|
||||
coordinator_daily: MetOfficeUpdateCoordinator,
|
||||
coordinator_hourly: MetOfficeUpdateCoordinator,
|
||||
coordinator_twice_daily: MetOfficeUpdateCoordinator,
|
||||
hass_data: dict[str, Any],
|
||||
hass_data: MetOfficeRuntimeData,
|
||||
) -> None:
|
||||
"""Initialise the platform with a data instance."""
|
||||
observation_coordinator = coordinator_hourly
|
||||
@@ -190,9 +188,9 @@ class MetOfficeWeather(
|
||||
)
|
||||
|
||||
self._attr_device_info = get_device_info(
|
||||
coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME]
|
||||
coordinates=hass_data.coordinates, name=hass_data.name
|
||||
)
|
||||
self._attr_unique_id = hass_data[METOFFICE_COORDINATES]
|
||||
self._attr_unique_id = hass_data.coordinates
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
|
||||
@@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MoatConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MoatConfigEntry) -> bool:
|
||||
"""Set up Moat BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = MoatBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
@@ -42,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MoatConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -4,12 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -28,7 +26,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MoatConfigEntry
|
||||
|
||||
SENSOR_DESCRIPTIONS = {
|
||||
(DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription(
|
||||
@@ -104,13 +102,11 @@ def sensor_update_to_bluetooth_data_update(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: MoatConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Moat BLE sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import State, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
@@ -95,7 +96,7 @@ class MobileAppEntity(RestoreEntity):
|
||||
config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON]
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return device_info(self._registration)
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ def webhook_response(
|
||||
)
|
||||
|
||||
|
||||
def device_info(registration: dict) -> DeviceInfo:
|
||||
def device_info(registration: Mapping[str, Any]) -> DeviceInfo:
|
||||
"""Return the device info for this registration."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""The Monoprice 6-Zone Amplifier integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pymonoprice import Monoprice, get_monoprice
|
||||
from pymonoprice import get_monoprice
|
||||
from serial import SerialException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -13,24 +10,14 @@ from homeassistant.const import CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_NOT_FIRST_RUN
|
||||
from .const import CONF_NOT_FIRST_RUN, DOMAIN, FIRST_RUN, MONOPRICE_OBJECT
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MonopriceConfigEntry = ConfigEntry[MonopriceRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonopriceRuntimeData:
|
||||
"""Data stored in the config entry for a Monoprice entry."""
|
||||
|
||||
client: Monoprice
|
||||
first_run: bool
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MonopriceConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Monoprice 6-Zone Amplifier from a config entry."""
|
||||
port = entry.data[CONF_PORT]
|
||||
|
||||
@@ -50,17 +37,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: MonopriceConfigEntry) ->
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_update_listener))
|
||||
|
||||
entry.runtime_data = MonopriceRuntimeData(
|
||||
client=monoprice,
|
||||
first_run=first_run,
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
MONOPRICE_OBJECT: monoprice,
|
||||
FIRST_RUN: first_run,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MonopriceConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if not unload_ok:
|
||||
@@ -74,7 +61,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: MonopriceConfigEntry) -
|
||||
"""
|
||||
del monoprice
|
||||
|
||||
await hass.async_add_executor_job(_cleanup, entry.runtime_data.client)
|
||||
monoprice = hass.data[DOMAIN][entry.entry_id][MONOPRICE_OBJECT]
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
await hass.async_add_executor_job(_cleanup, monoprice)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -15,3 +15,6 @@ CONF_NOT_FIRST_RUN = "not_first_run"
|
||||
|
||||
SERVICE_SNAPSHOT = "snapshot"
|
||||
SERVICE_RESTORE = "restore"
|
||||
|
||||
FIRST_RUN = "first_run"
|
||||
MONOPRICE_OBJECT = "monoprice_object"
|
||||
|
||||
@@ -11,14 +11,21 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform, service
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MonopriceConfigEntry
|
||||
from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT
|
||||
from .const import (
|
||||
CONF_SOURCES,
|
||||
DOMAIN,
|
||||
FIRST_RUN,
|
||||
MONOPRICE_OBJECT,
|
||||
SERVICE_RESTORE,
|
||||
SERVICE_SNAPSHOT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,13 +57,13 @@ def _get_sources(config_entry):
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MonopriceConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Monoprice 6-zone amplifier platform."""
|
||||
port = config_entry.data[CONF_PORT]
|
||||
|
||||
monoprice = config_entry.runtime_data.client
|
||||
monoprice = hass.data[DOMAIN][config_entry.entry_id][MONOPRICE_OBJECT]
|
||||
|
||||
sources = _get_sources(config_entry)
|
||||
|
||||
@@ -70,7 +77,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# only call update before add if it's the first run so we can try to detect zones
|
||||
async_add_entities(entities, config_entry.runtime_data.first_run)
|
||||
first_run = hass.data[DOMAIN][config_entry.entry_id][FIRST_RUN]
|
||||
async_add_entities(entities, first_run)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -14,15 +13,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from .api import AuthenticatedMonzoAPI
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MonzoCoordinator
|
||||
from .coordinator import MonzoConfigEntry, MonzoCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
|
||||
|
||||
@@ -39,7 +37,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Set up Monzo from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
|
||||
@@ -51,15 +49,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The Monzo integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -18,6 +20,8 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MonzoConfigEntry = ConfigEntry[MonzoCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonzoData:
|
||||
@@ -30,10 +34,13 @@ class MonzoData:
|
||||
class MonzoCoordinator(DataUpdateCoordinator[MonzoData]):
|
||||
"""Class to manage fetching Monzo data from the API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MonzoConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, api: AuthenticatedMonzoAPI
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MonzoConfigEntry,
|
||||
api: AuthenticatedMonzoAPI,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
|
||||
@@ -11,14 +11,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import MonzoCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MonzoData
|
||||
from .coordinator import MonzoConfigEntry, MonzoCoordinator, MonzoData
|
||||
from .entity import MonzoBaseEntity
|
||||
|
||||
|
||||
@@ -64,11 +61,11 @@ MODEL_POT = "Pot"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MonzoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Defer sensor setup to the shared sensor module."""
|
||||
coordinator: MonzoCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
accounts = [
|
||||
MonzoSensor(
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .coordinator import NRGkickConfigEntry, NRGkickDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
||||
76
homeassistant/components/nrgkick/binary_sensor.py
Normal file
76
homeassistant/components/nrgkick/binary_sensor.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Binary sensor platform for NRGkick."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import NRGkickConfigEntry, NRGkickData, NRGkickDataUpdateCoordinator
|
||||
from .entity import NRGkickEntity, get_nested_dict_value
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NRGkickBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class describing NRGkick binary sensor entities."""
|
||||
|
||||
is_on_fn: Callable[[NRGkickData], bool | None]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[NRGkickBinarySensorEntityDescription, ...] = (
|
||||
NRGkickBinarySensorEntityDescription(
|
||||
key="charge_permitted",
|
||||
translation_key="charge_permitted",
|
||||
is_on_fn=lambda data: (
|
||||
bool(value)
|
||||
if (
|
||||
value := get_nested_dict_value(
|
||||
data.values, "general", "charge_permitted"
|
||||
)
|
||||
)
|
||||
is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
_hass: HomeAssistant,
|
||||
entry: NRGkickConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up NRGkick binary sensors based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
NRGkickBinarySensor(coordinator, description) for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class NRGkickBinarySensor(NRGkickEntity, BinarySensorEntity):
|
||||
"""Representation of a NRGkick binary sensor."""
|
||||
|
||||
entity_description: NRGkickBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NRGkickDataUpdateCoordinator,
|
||||
entity_description: NRGkickBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(coordinator, entity_description.key)
|
||||
self.entity_description = entity_description
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the binary sensor."""
|
||||
return self.entity_description.is_on_fn(self.coordinator.data)
|
||||
@@ -14,6 +14,17 @@ from .const import DOMAIN
|
||||
from .coordinator import NRGkickDataUpdateCoordinator
|
||||
|
||||
|
||||
def get_nested_dict_value(data: Any, *keys: str) -> Any:
|
||||
"""Safely get a nested value from dict-like API responses."""
|
||||
current: Any = data
|
||||
for key in keys:
|
||||
try:
|
||||
current = current.get(key)
|
||||
except AttributeError:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
class NRGkickEntity(CoordinatorEntity[NRGkickDataUpdateCoordinator]):
|
||||
"""Base class for NRGkick entities with common device info setup."""
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"charge_permitted": {
|
||||
"default": "mdi:ev-station"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"current_set": {
|
||||
"default": "mdi:current-ac"
|
||||
|
||||
@@ -45,22 +45,11 @@ from .const import (
|
||||
WARNING_CODE_MAP,
|
||||
)
|
||||
from .coordinator import NRGkickConfigEntry, NRGkickData, NRGkickDataUpdateCoordinator
|
||||
from .entity import NRGkickEntity
|
||||
from .entity import NRGkickEntity, get_nested_dict_value
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def _get_nested_dict_value(data: Any, *keys: str) -> Any:
|
||||
"""Safely get a nested value from dict-like API responses."""
|
||||
current: Any = data
|
||||
for key in keys:
|
||||
try:
|
||||
current = current.get(key)
|
||||
except AttributeError:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NRGkickSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing NRGkick sensor entities."""
|
||||
@@ -159,7 +148,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.info, "general", "rated_current"
|
||||
),
|
||||
),
|
||||
@@ -167,7 +156,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
NRGkickSensorEntityDescription(
|
||||
key="connector_phase_count",
|
||||
translation_key="connector_phase_count",
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.info, "connector", "phase_count"
|
||||
),
|
||||
),
|
||||
@@ -178,7 +167,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.info, "connector", "max_current"
|
||||
),
|
||||
),
|
||||
@@ -189,7 +178,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
options=_enum_options_from_mapping(CONNECTOR_TYPE_MAP),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _map_code_to_translation_key(
|
||||
cast(StateType, _get_nested_dict_value(data.info, "connector", "type")),
|
||||
cast(StateType, get_nested_dict_value(data.info, "connector", "type")),
|
||||
CONNECTOR_TYPE_MAP,
|
||||
),
|
||||
),
|
||||
@@ -198,7 +187,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
translation_key="connector_serial",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "connector", "serial"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "connector", "serial"),
|
||||
),
|
||||
# INFO - Grid
|
||||
NRGkickSensorEntityDescription(
|
||||
@@ -208,7 +197,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "grid", "voltage"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "grid", "voltage"),
|
||||
),
|
||||
NRGkickSensorEntityDescription(
|
||||
key="grid_frequency",
|
||||
@@ -217,7 +206,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "grid", "frequency"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "grid", "frequency"),
|
||||
),
|
||||
# INFO - Network
|
||||
NRGkickSensorEntityDescription(
|
||||
@@ -225,7 +214,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
translation_key="network_ssid",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "network", "ssid"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "network", "ssid"),
|
||||
),
|
||||
NRGkickSensorEntityDescription(
|
||||
key="network_rssi",
|
||||
@@ -234,7 +223,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "network", "rssi"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "network", "rssi"),
|
||||
),
|
||||
# INFO - Cellular (optional, only if cellular module is available)
|
||||
NRGkickSensorEntityDescription(
|
||||
@@ -246,7 +235,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
requires_sim_module=True,
|
||||
value_fn=lambda data: _map_code_to_translation_key(
|
||||
cast(StateType, _get_nested_dict_value(data.info, "cellular", "mode")),
|
||||
cast(StateType, get_nested_dict_value(data.info, "cellular", "mode")),
|
||||
CELLULAR_MODE_MAP,
|
||||
),
|
||||
),
|
||||
@@ -259,7 +248,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
requires_sim_module=True,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "cellular", "rssi"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "cellular", "rssi"),
|
||||
),
|
||||
NRGkickSensorEntityDescription(
|
||||
key="cellular_operator",
|
||||
@@ -267,7 +256,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
requires_sim_module=True,
|
||||
value_fn=lambda data: _get_nested_dict_value(data.info, "cellular", "operator"),
|
||||
value_fn=lambda data: get_nested_dict_value(data.info, "cellular", "operator"),
|
||||
),
|
||||
# VALUES - Energy
|
||||
NRGkickSensorEntityDescription(
|
||||
@@ -278,7 +267,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "energy", "total_charged_energy"
|
||||
),
|
||||
),
|
||||
@@ -290,7 +279,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "energy", "charged_energy"
|
||||
),
|
||||
),
|
||||
@@ -302,7 +291,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "charging_voltage"
|
||||
),
|
||||
),
|
||||
@@ -313,7 +302,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "charging_current"
|
||||
),
|
||||
),
|
||||
@@ -326,7 +315,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "grid_frequency"
|
||||
),
|
||||
),
|
||||
@@ -339,7 +328,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "peak_power"
|
||||
),
|
||||
),
|
||||
@@ -350,7 +339,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "total_active_power"
|
||||
),
|
||||
),
|
||||
@@ -362,7 +351,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "total_reactive_power"
|
||||
),
|
||||
),
|
||||
@@ -374,7 +363,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "total_apparent_power"
|
||||
),
|
||||
),
|
||||
@@ -386,7 +375,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "total_power_factor"
|
||||
),
|
||||
),
|
||||
@@ -400,7 +389,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l1", "voltage"
|
||||
),
|
||||
),
|
||||
@@ -411,7 +400,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l1", "current"
|
||||
),
|
||||
),
|
||||
@@ -422,7 +411,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l1", "active_power"
|
||||
),
|
||||
),
|
||||
@@ -434,7 +423,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l1", "reactive_power"
|
||||
),
|
||||
),
|
||||
@@ -446,7 +435,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l1", "apparent_power"
|
||||
),
|
||||
),
|
||||
@@ -458,7 +447,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l1", "power_factor"
|
||||
),
|
||||
),
|
||||
@@ -472,7 +461,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l2", "voltage"
|
||||
),
|
||||
),
|
||||
@@ -483,7 +472,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l2", "current"
|
||||
),
|
||||
),
|
||||
@@ -494,7 +483,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l2", "active_power"
|
||||
),
|
||||
),
|
||||
@@ -506,7 +495,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l2", "reactive_power"
|
||||
),
|
||||
),
|
||||
@@ -518,7 +507,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l2", "apparent_power"
|
||||
),
|
||||
),
|
||||
@@ -530,7 +519,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l2", "power_factor"
|
||||
),
|
||||
),
|
||||
@@ -544,7 +533,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l3", "voltage"
|
||||
),
|
||||
),
|
||||
@@ -555,7 +544,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l3", "current"
|
||||
),
|
||||
),
|
||||
@@ -566,7 +555,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l3", "active_power"
|
||||
),
|
||||
),
|
||||
@@ -578,7 +567,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l3", "reactive_power"
|
||||
),
|
||||
),
|
||||
@@ -590,7 +579,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l3", "apparent_power"
|
||||
),
|
||||
),
|
||||
@@ -602,7 +591,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "l3", "power_factor"
|
||||
),
|
||||
),
|
||||
@@ -616,7 +605,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "powerflow", "n", "current"
|
||||
),
|
||||
),
|
||||
@@ -626,7 +615,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
translation_key="charging_rate",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "general", "charging_rate"
|
||||
),
|
||||
),
|
||||
@@ -638,12 +627,12 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
_seconds_to_stable_timestamp(
|
||||
cast(
|
||||
StateType,
|
||||
_get_nested_dict_value(
|
||||
get_nested_dict_value(
|
||||
data.values, "general", "vehicle_connect_time"
|
||||
),
|
||||
)
|
||||
)
|
||||
if _get_nested_dict_value(data.values, "general", "status")
|
||||
if get_nested_dict_value(data.values, "general", "status")
|
||||
!= ChargingStatus.STANDBY
|
||||
else None
|
||||
),
|
||||
@@ -655,7 +644,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "general", "vehicle_charging_time"
|
||||
),
|
||||
),
|
||||
@@ -665,7 +654,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=_enum_options_from_mapping(STATUS_MAP),
|
||||
value_fn=lambda data: _map_code_to_translation_key(
|
||||
cast(StateType, _get_nested_dict_value(data.values, "general", "status")),
|
||||
cast(StateType, get_nested_dict_value(data.values, "general", "status")),
|
||||
STATUS_MAP,
|
||||
),
|
||||
),
|
||||
@@ -675,7 +664,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "general", "charge_count"
|
||||
),
|
||||
),
|
||||
@@ -687,7 +676,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _map_code_to_translation_key(
|
||||
cast(
|
||||
StateType, _get_nested_dict_value(data.values, "general", "rcd_trigger")
|
||||
StateType, get_nested_dict_value(data.values, "general", "rcd_trigger")
|
||||
),
|
||||
RCD_TRIGGER_MAP,
|
||||
),
|
||||
@@ -700,8 +689,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _map_code_to_translation_key(
|
||||
cast(
|
||||
StateType,
|
||||
_get_nested_dict_value(data.values, "general", "warning_code"),
|
||||
StateType, get_nested_dict_value(data.values, "general", "warning_code")
|
||||
),
|
||||
WARNING_CODE_MAP,
|
||||
),
|
||||
@@ -714,7 +702,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _map_code_to_translation_key(
|
||||
cast(
|
||||
StateType, _get_nested_dict_value(data.values, "general", "error_code")
|
||||
StateType, get_nested_dict_value(data.values, "general", "error_code")
|
||||
),
|
||||
ERROR_CODE_MAP,
|
||||
),
|
||||
@@ -727,7 +715,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "temperatures", "housing"
|
||||
),
|
||||
),
|
||||
@@ -738,7 +726,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "temperatures", "connector_l1"
|
||||
),
|
||||
),
|
||||
@@ -749,7 +737,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "temperatures", "connector_l2"
|
||||
),
|
||||
),
|
||||
@@ -760,7 +748,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "temperatures", "connector_l3"
|
||||
),
|
||||
),
|
||||
@@ -771,7 +759,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "temperatures", "domestic_plug_1"
|
||||
),
|
||||
),
|
||||
@@ -782,7 +770,7 @@ SENSORS: tuple[NRGkickSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: _get_nested_dict_value(
|
||||
value_fn=lambda data: get_nested_dict_value(
|
||||
data.values, "temperatures", "domestic_plug_2"
|
||||
),
|
||||
),
|
||||
|
||||
@@ -78,6 +78,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"charge_permitted": {
|
||||
"name": "Charge permitted"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"current_set": {
|
||||
"name": "Charging current"
|
||||
|
||||
@@ -16,23 +16,30 @@ from onvif.client import (
|
||||
)
|
||||
from onvif.exceptions import ONVIFError
|
||||
from onvif.util import stringify_onvif_error
|
||||
import onvif_parsers
|
||||
from zeep.exceptions import Fault, TransportError, ValidationError, XMLParseError
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .models import Event, PullPointManagerState, WebHookManagerState
|
||||
from .parsers import PARSERS
|
||||
|
||||
# Topics in this list are ignored because we do not want to create
|
||||
# entities for them.
|
||||
UNHANDLED_TOPICS: set[str] = {"tns1:MediaControl/VideoEncoderConfiguration"}
|
||||
|
||||
ENTITY_CATEGORY_MAPPING: dict[str, EntityCategory] = {
|
||||
"diagnostic": EntityCategory.DIAGNOSTIC,
|
||||
"config": EntityCategory.CONFIG,
|
||||
}
|
||||
|
||||
SUBSCRIPTION_ERRORS = (Fault, TimeoutError, TransportError)
|
||||
CREATE_ERRORS = (
|
||||
ONVIFError,
|
||||
@@ -81,6 +88,18 @@ PULLPOINT_MESSAGE_LIMIT = 100
|
||||
PULLPOINT_COOLDOWN_TIME = 0.75
|
||||
|
||||
|
||||
def _local_datetime_or_none(value: str) -> dt.datetime | None:
|
||||
"""Convert strings to datetimes, if invalid, return None."""
|
||||
# Handle cameras that return times like '0000-00-00T00:00:00Z' (e.g. Hikvision)
|
||||
try:
|
||||
ret = dt_util.parse_datetime(value)
|
||||
except ValueError:
|
||||
return None
|
||||
if ret is not None:
|
||||
return dt_util.as_local(ret)
|
||||
return None
|
||||
|
||||
|
||||
class EventManager:
|
||||
"""ONVIF Event Manager."""
|
||||
|
||||
@@ -176,7 +195,10 @@ class EventManager:
|
||||
# tns1:RuleEngine/CellMotionDetector/Motion
|
||||
topic = msg.Topic._value_1.rstrip("/.") # noqa: SLF001
|
||||
|
||||
if not (parser := PARSERS.get(topic)):
|
||||
try:
|
||||
event = await onvif_parsers.parse(topic, unique_id, msg)
|
||||
error = None
|
||||
except onvif_parsers.errors.UnknownTopicError:
|
||||
if topic not in UNHANDLED_TOPICS:
|
||||
LOGGER.warning(
|
||||
"%s: No registered handler for event from %s: %s",
|
||||
@@ -186,10 +208,6 @@ class EventManager:
|
||||
)
|
||||
UNHANDLED_TOPICS.add(topic)
|
||||
continue
|
||||
|
||||
try:
|
||||
event = await parser(unique_id, msg)
|
||||
error = None
|
||||
except (AttributeError, KeyError) as e:
|
||||
event = None
|
||||
error = e
|
||||
@@ -202,10 +220,26 @@ class EventManager:
|
||||
error,
|
||||
msg,
|
||||
)
|
||||
return
|
||||
continue
|
||||
|
||||
self.get_uids_by_platform(event.platform).add(event.uid)
|
||||
self._events[event.uid] = event
|
||||
value = event.value
|
||||
if event.device_class == "timestamp" and isinstance(value, str):
|
||||
value = _local_datetime_or_none(value)
|
||||
|
||||
ha_event = Event(
|
||||
uid=event.uid,
|
||||
name=event.name,
|
||||
platform=event.platform,
|
||||
device_class=event.device_class,
|
||||
unit_of_measurement=event.unit_of_measurement,
|
||||
value=value,
|
||||
entity_category=ENTITY_CATEGORY_MAPPING.get(
|
||||
event.entity_category or ""
|
||||
),
|
||||
entity_enabled=event.entity_enabled,
|
||||
)
|
||||
self.get_uids_by_platform(ha_event.platform).add(ha_event.uid)
|
||||
self._events[ha_event.uid] = ha_event
|
||||
|
||||
def get_uid(self, uid: str) -> Event | None:
|
||||
"""Retrieve event for given id."""
|
||||
|
||||
@@ -13,5 +13,9 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["onvif", "wsdiscovery", "zeep"],
|
||||
"requirements": ["onvif-zeep-async==4.0.4", "WSDiscovery==2.1.2"]
|
||||
"requirements": [
|
||||
"onvif-zeep-async==4.0.4",
|
||||
"onvif_parsers==1.2.2",
|
||||
"WSDiscovery==2.1.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,755 +0,0 @@
|
||||
"""ONVIF event parsers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
import dataclasses
|
||||
import datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .models import Event
|
||||
|
||||
PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event | None]]] = (
|
||||
Registry()
|
||||
)
|
||||
|
||||
VIDEO_SOURCE_MAPPING = {
|
||||
"vsconf": "VideoSourceToken",
|
||||
}
|
||||
|
||||
|
||||
def extract_message(msg: Any) -> tuple[str, Any]:
|
||||
"""Extract the message content and the topic."""
|
||||
return msg.Topic._value_1, msg.Message._value_1 # noqa: SLF001
|
||||
|
||||
|
||||
def _normalize_video_source(source: str) -> str:
|
||||
"""Normalize video source.
|
||||
|
||||
Some cameras do not set the VideoSourceToken correctly so we get duplicate
|
||||
sensors, so we need to normalize it to the correct value.
|
||||
"""
|
||||
return VIDEO_SOURCE_MAPPING.get(source, source)
|
||||
|
||||
|
||||
def local_datetime_or_none(value: str) -> datetime.datetime | None:
|
||||
"""Convert strings to datetimes, if invalid, return None."""
|
||||
# To handle cameras that return times like '0000-00-00T00:00:00Z' (e.g. hikvision)
|
||||
try:
|
||||
ret = dt_util.parse_datetime(value)
|
||||
except ValueError:
|
||||
return None
|
||||
if ret is not None:
|
||||
return dt_util.as_local(ret)
|
||||
return None
|
||||
|
||||
|
||||
@PARSERS.register("tns1:VideoSource/MotionAlarm")
|
||||
@PARSERS.register("tns1:Device/Trigger/tnshik:AlarmIn")
|
||||
async def async_parse_motion_alarm(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:VideoSource/MotionAlarm
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Motion Alarm",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooBlurry/AnalyticsService")
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooBlurry/ImagingService")
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooBlurry/RecordingService")
|
||||
async def async_parse_image_too_blurry(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:VideoSource/ImageTooBlurry/*
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Image Too Blurry",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooDark/AnalyticsService")
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooDark/ImagingService")
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooDark/RecordingService")
|
||||
async def async_parse_image_too_dark(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:VideoSource/ImageTooDark/*
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Image Too Dark",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooBright/AnalyticsService")
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooBright/ImagingService")
|
||||
@PARSERS.register("tns1:VideoSource/ImageTooBright/RecordingService")
|
||||
async def async_parse_image_too_bright(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:VideoSource/ImageTooBright/*
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Image Too Bright",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:VideoSource/GlobalSceneChange/AnalyticsService")
|
||||
@PARSERS.register("tns1:VideoSource/GlobalSceneChange/ImagingService")
|
||||
@PARSERS.register("tns1:VideoSource/GlobalSceneChange/RecordingService")
|
||||
async def async_parse_scene_change(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:VideoSource/GlobalSceneChange/*
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Global Scene Change",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:AudioAnalytics/Audio/DetectedSound")
|
||||
async def async_parse_detected_sound(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:AudioAnalytics/Audio/DetectedSound
|
||||
"""
|
||||
audio_source = ""
|
||||
audio_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "AudioSourceConfigurationToken":
|
||||
audio_source = source.Value
|
||||
if source.Name == "AudioAnalyticsConfigurationToken":
|
||||
audio_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{audio_source}_{audio_analytics}_{rule}",
|
||||
"Detected Sound",
|
||||
"binary_sensor",
|
||||
"sound",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/FieldDetector/ObjectsInside")
|
||||
async def async_parse_field_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/FieldDetector/ObjectsInside
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
"Field Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/Motion")
|
||||
async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/CellMotionDetector/Motion
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
"Cell Motion Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MotionRegionDetector/Motion")
|
||||
async def async_parse_motion_region_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MotionRegionDetector/Motion
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
"Motion Region Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value in ["1", "true"],
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/TamperDetector/Tamper")
|
||||
async def async_parse_tamper_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/TamperDetector/Tamper
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
"Tamper Detection",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/DogCatDetect")
|
||||
async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MyRuleDetector/DogCatDetect
|
||||
"""
|
||||
video_source = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Pet Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/VehicleDetect")
|
||||
async def async_parse_vehicle_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MyRuleDetector/VehicleDetect
|
||||
"""
|
||||
video_source = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Vehicle Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
_TAPO_EVENT_TEMPLATES: dict[str, Event] = {
|
||||
"IsVehicle": Event(
|
||||
uid="",
|
||||
name="Vehicle Detection",
|
||||
platform="binary_sensor",
|
||||
device_class="motion",
|
||||
),
|
||||
"IsPeople": Event(
|
||||
uid="", name="Person Detection", platform="binary_sensor", device_class="motion"
|
||||
),
|
||||
"IsPet": Event(
|
||||
uid="", name="Pet Detection", platform="binary_sensor", device_class="motion"
|
||||
),
|
||||
"IsLineCross": Event(
|
||||
uid="",
|
||||
name="Line Detector Crossed",
|
||||
platform="binary_sensor",
|
||||
device_class="motion",
|
||||
),
|
||||
"IsTamper": Event(
|
||||
uid="", name="Tamper Detection", platform="binary_sensor", device_class="tamper"
|
||||
),
|
||||
"IsIntrusion": Event(
|
||||
uid="",
|
||||
name="Intrusion Detection",
|
||||
platform="binary_sensor",
|
||||
device_class="safety",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/Intrusion")
|
||||
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/LineCross")
|
||||
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/People")
|
||||
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/Tamper")
|
||||
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/TpSmartEvent")
|
||||
@PARSERS.register("tns1:RuleEngine/PeopleDetector/People")
|
||||
@PARSERS.register("tns1:RuleEngine/TPSmartEventDetector/TPSmartEvent")
|
||||
async def async_parse_tplink_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing tplink smart event messages.
|
||||
|
||||
Topic: tns1:RuleEngine/CellMotionDetector/Intrusion
|
||||
Topic: tns1:RuleEngine/CellMotionDetector/LineCross
|
||||
Topic: tns1:RuleEngine/CellMotionDetector/People
|
||||
Topic: tns1:RuleEngine/CellMotionDetector/Tamper
|
||||
Topic: tns1:RuleEngine/CellMotionDetector/TpSmartEvent
|
||||
Topic: tns1:RuleEngine/PeopleDetector/People
|
||||
Topic: tns1:RuleEngine/TPSmartEventDetector/TPSmartEvent
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
for item in payload.Data.SimpleItem:
|
||||
event_template = _TAPO_EVENT_TEMPLATES.get(item.Name)
|
||||
if event_template is None:
|
||||
continue
|
||||
|
||||
return dataclasses.replace(
|
||||
event_template,
|
||||
uid=f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
value=item.Value == "true",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/PeopleDetect")
|
||||
async def async_parse_person_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MyRuleDetector/PeopleDetect
|
||||
"""
|
||||
video_source = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Person Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/FaceDetect")
|
||||
async def async_parse_face_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MyRuleDetector/FaceDetect
|
||||
"""
|
||||
video_source = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Face Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/Visitor")
|
||||
async def async_parse_visitor_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MyRuleDetector/Visitor
|
||||
"""
|
||||
video_source = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Visitor Detection",
|
||||
"binary_sensor",
|
||||
"occupancy",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/Package")
|
||||
async def async_parse_package_detector(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/MyRuleDetector/Package
|
||||
"""
|
||||
video_source = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Package Detection",
|
||||
"binary_sensor",
|
||||
"occupancy",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Device/Trigger/DigitalInput")
|
||||
async def async_parse_digital_input(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Device/Trigger/DigitalInput
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Digital Input",
|
||||
"binary_sensor",
|
||||
None,
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Device/Trigger/Relay")
|
||||
async def async_parse_relay(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Device/Trigger/Relay
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Relay Triggered",
|
||||
"binary_sensor",
|
||||
None,
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "active",
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Device/HardwareFailure/StorageFailure")
|
||||
async def async_parse_storage_failure(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Device/HardwareFailure/StorageFailure
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Storage Failure",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Monitoring/ProcessorUsage")
|
||||
async def async_parse_processor_usage(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Monitoring/ProcessorUsage
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
usage = float(payload.Data.SimpleItem[0].Value)
|
||||
if usage <= 1:
|
||||
usage *= 100
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}",
|
||||
"Processor Usage",
|
||||
"sensor",
|
||||
None,
|
||||
"percent",
|
||||
int(usage),
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Monitoring/OperatingTime/LastReboot")
|
||||
async def async_parse_last_reboot(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Monitoring/OperatingTime/LastReboot
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
date_time = local_datetime_or_none(payload.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{topic}",
|
||||
"Last Reboot",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
None,
|
||||
date_time,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Monitoring/OperatingTime/LastReset")
|
||||
async def async_parse_last_reset(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Monitoring/OperatingTime/LastReset
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
date_time = local_datetime_or_none(payload.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{topic}",
|
||||
"Last Reset",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
None,
|
||||
date_time,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
entity_enabled=False,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Monitoring/Backup/Last")
|
||||
async def async_parse_backup_last(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Monitoring/Backup/Last
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
date_time = local_datetime_or_none(payload.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{topic}",
|
||||
"Last Backup",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
None,
|
||||
date_time,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
entity_enabled=False,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:Monitoring/OperatingTime/LastClockSynchronization")
|
||||
async def async_parse_last_clock_sync(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
date_time = local_datetime_or_none(payload.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{topic}",
|
||||
"Last Clock Synchronization",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
None,
|
||||
date_time,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
entity_enabled=False,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RecordingConfig/JobState")
|
||||
async def async_parse_jobstate(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RecordingConfig/JobState
|
||||
"""
|
||||
|
||||
topic, payload = extract_message(msg)
|
||||
source = payload.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{topic}_{source}",
|
||||
"Recording Job State",
|
||||
"binary_sensor",
|
||||
None,
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "Active",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/LineDetector/Crossed")
|
||||
async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/LineDetector/Crossed
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = source.Value
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
"Line Detector Crossed",
|
||||
"sensor",
|
||||
None,
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:RuleEngine/CountAggregation/Counter")
|
||||
async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:RuleEngine/CountAggregation/Counter
|
||||
"""
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
topic, payload = extract_message(msg)
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
video_analytics = source.Value
|
||||
if source.Name == "Rule":
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}_{video_analytics}_{rule}",
|
||||
"Count Aggregation Counter",
|
||||
"sensor",
|
||||
None,
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
|
||||
|
||||
@PARSERS.register("tns1:UserAlarm/IVA/HumanShapeDetect")
|
||||
async def async_parse_human_shape_detect(uid: str, msg) -> Event | None:
|
||||
"""Handle parsing event message.
|
||||
|
||||
Topic: tns1:UserAlarm/IVA/HumanShapeDetect
|
||||
"""
|
||||
topic, payload = extract_message(msg)
|
||||
video_source = ""
|
||||
for source in payload.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
break
|
||||
|
||||
return Event(
|
||||
f"{uid}_{topic}_{video_source}",
|
||||
"Human Shape Detect",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
payload.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
@@ -9,6 +9,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["openevsehttp"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-openevse-http==0.2.1"],
|
||||
"requirements": ["python-openevse-http==0.2.5"],
|
||||
"zeroconf": ["_openevse._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.5.5"]
|
||||
"requirements": ["renault-api==0.5.6"]
|
||||
}
|
||||
|
||||
@@ -2,35 +2,17 @@
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_CODE, CONF_HOST, CONF_NAME, CONF_PORT, Platform
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .client import SatelClient
|
||||
from .const import (
|
||||
CONF_ARM_HOME_MODE,
|
||||
CONF_DEVICE_PARTITIONS,
|
||||
CONF_OUTPUT_NUMBER,
|
||||
CONF_OUTPUTS,
|
||||
CONF_PARTITION_NUMBER,
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER,
|
||||
CONF_SWITCHABLE_OUTPUTS,
|
||||
CONF_ZONE_NUMBER,
|
||||
CONF_ZONE_TYPE,
|
||||
CONF_ZONES,
|
||||
DEFAULT_CONF_ARM_HOME_MODE,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_ZONE_TYPE,
|
||||
DOMAIN,
|
||||
SUBENTRY_TYPE_OUTPUT,
|
||||
SUBENTRY_TYPE_PARTITION,
|
||||
@@ -49,104 +31,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string,
|
||||
}
|
||||
)
|
||||
EDITABLE_OUTPUT_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string})
|
||||
PARTITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ARM_HOME_MODE, default=DEFAULT_CONF_ARM_HOME_MODE): vol.In(
|
||||
[1, 2, 3]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def is_alarm_code_necessary(value):
|
||||
"""Check if alarm code must be configured."""
|
||||
if value.get(CONF_SWITCHABLE_OUTPUTS) and CONF_CODE not in value:
|
||||
raise vol.Invalid("You need to specify alarm code to use switchable_outputs")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_DEVICE_PARTITIONS, default={}): {
|
||||
vol.Coerce(int): PARTITION_SCHEMA
|
||||
},
|
||||
vol.Optional(CONF_ZONES, default={}): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_OUTPUTS, default={}): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_SWITCHABLE_OUTPUTS, default={}): {
|
||||
vol.Coerce(int): EDITABLE_OUTPUT_SCHEMA
|
||||
},
|
||||
},
|
||||
is_alarm_code_necessary,
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up Satel Integra from YAML."""
|
||||
|
||||
if config := hass_config.get(DOMAIN):
|
||||
hass.async_create_task(_async_import(hass, config))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Process YAML import."""
|
||||
|
||||
if not hass.config_entries.async_entries(DOMAIN):
|
||||
# Start import flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
|
||||
if result.get("type") == FlowResultType.ABORT:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_cannot_connect",
|
||||
breaks_in_ha_version="2026.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_cannot_connect",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Satel Integra",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2026.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Satel Integra",
|
||||
},
|
||||
)
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SatelConfigEntry) -> bool:
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryData,
|
||||
ConfigSubentryFlow,
|
||||
OptionsFlow,
|
||||
SubentryFlowResult,
|
||||
@@ -24,15 +23,11 @@ from homeassistant.helpers import config_validation as cv, selector
|
||||
|
||||
from .const import (
|
||||
CONF_ARM_HOME_MODE,
|
||||
CONF_DEVICE_PARTITIONS,
|
||||
CONF_OUTPUT_NUMBER,
|
||||
CONF_OUTPUTS,
|
||||
CONF_PARTITION_NUMBER,
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER,
|
||||
CONF_SWITCHABLE_OUTPUTS,
|
||||
CONF_ZONE_NUMBER,
|
||||
CONF_ZONE_TYPE,
|
||||
CONF_ZONES,
|
||||
DEFAULT_CONF_ARM_HOME_MODE,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
@@ -53,6 +48,7 @@ CONNECTION_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CODE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
@@ -143,97 +139,6 @@ class SatelConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=CONNECTION_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self, import_config: dict[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by import."""
|
||||
|
||||
valid = await self.test_connection(
|
||||
import_config[CONF_HOST], import_config.get(CONF_PORT, DEFAULT_PORT)
|
||||
)
|
||||
|
||||
if valid:
|
||||
subentries: list[ConfigSubentryData] = []
|
||||
|
||||
for partition_number, partition_data in import_config.get(
|
||||
CONF_DEVICE_PARTITIONS, {}
|
||||
).items():
|
||||
subentries.append(
|
||||
{
|
||||
"subentry_type": SUBENTRY_TYPE_PARTITION,
|
||||
"title": f"{partition_data[CONF_NAME]} ({partition_number})",
|
||||
"unique_id": f"{SUBENTRY_TYPE_PARTITION}_{partition_number}",
|
||||
"data": {
|
||||
CONF_NAME: partition_data[CONF_NAME],
|
||||
CONF_ARM_HOME_MODE: partition_data.get(
|
||||
CONF_ARM_HOME_MODE, DEFAULT_CONF_ARM_HOME_MODE
|
||||
),
|
||||
CONF_PARTITION_NUMBER: partition_number,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for zone_number, zone_data in import_config.get(CONF_ZONES, {}).items():
|
||||
subentries.append(
|
||||
{
|
||||
"subentry_type": SUBENTRY_TYPE_ZONE,
|
||||
"title": f"{zone_data[CONF_NAME]} ({zone_number})",
|
||||
"unique_id": f"{SUBENTRY_TYPE_ZONE}_{zone_number}",
|
||||
"data": {
|
||||
CONF_NAME: zone_data[CONF_NAME],
|
||||
CONF_ZONE_NUMBER: zone_number,
|
||||
CONF_ZONE_TYPE: zone_data.get(
|
||||
CONF_ZONE_TYPE, BinarySensorDeviceClass.MOTION
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for output_number, output_data in import_config.get(
|
||||
CONF_OUTPUTS, {}
|
||||
).items():
|
||||
subentries.append(
|
||||
{
|
||||
"subentry_type": SUBENTRY_TYPE_OUTPUT,
|
||||
"title": f"{output_data[CONF_NAME]} ({output_number})",
|
||||
"unique_id": f"{SUBENTRY_TYPE_OUTPUT}_{output_number}",
|
||||
"data": {
|
||||
CONF_NAME: output_data[CONF_NAME],
|
||||
CONF_OUTPUT_NUMBER: output_number,
|
||||
CONF_ZONE_TYPE: output_data.get(
|
||||
CONF_ZONE_TYPE, BinarySensorDeviceClass.MOTION
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for switchable_output_number, switchable_output_data in import_config.get(
|
||||
CONF_SWITCHABLE_OUTPUTS, {}
|
||||
).items():
|
||||
subentries.append(
|
||||
{
|
||||
"subentry_type": SUBENTRY_TYPE_SWITCHABLE_OUTPUT,
|
||||
"title": f"{switchable_output_data[CONF_NAME]} ({switchable_output_number})",
|
||||
"unique_id": f"{SUBENTRY_TYPE_SWITCHABLE_OUTPUT}_{switchable_output_number}",
|
||||
"data": {
|
||||
CONF_NAME: switchable_output_data[CONF_NAME],
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER: switchable_output_number,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=import_config[CONF_HOST],
|
||||
data={
|
||||
CONF_HOST: import_config[CONF_HOST],
|
||||
CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT),
|
||||
},
|
||||
options={CONF_CODE: import_config.get(CONF_CODE)},
|
||||
subentries=subentries,
|
||||
)
|
||||
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
async def test_connection(self, host: str, port: int) -> bool:
|
||||
"""Test a connection to the Satel alarm."""
|
||||
controller = AsyncSatel(host, port, self.hass.loop)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
DEFAULT_CONF_ARM_HOME_MODE = 1
|
||||
DEFAULT_PORT = 7094
|
||||
DEFAULT_ZONE_TYPE = "motion"
|
||||
|
||||
DOMAIN = "satel_integra"
|
||||
|
||||
@@ -16,11 +15,7 @@ CONF_ZONE_NUMBER = "zone_number"
|
||||
CONF_OUTPUT_NUMBER = "output_number"
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER = "switchable_output_number"
|
||||
|
||||
CONF_DEVICE_PARTITIONS = "partitions"
|
||||
CONF_ARM_HOME_MODE = "arm_home_mode"
|
||||
CONF_ZONE_TYPE = "type"
|
||||
CONF_ZONES = "zones"
|
||||
CONF_OUTPUTS = "outputs"
|
||||
CONF_SWITCHABLE_OUTPUTS = "switchable_outputs"
|
||||
|
||||
ZONES = "zones"
|
||||
|
||||
@@ -167,12 +167,6 @@
|
||||
"message": "Cannot control switchable outputs because no user code is configured for this Satel Integra entry. Configure a code in the integration options to enable output control."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your existing configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the `{domain}` YAML configuration from your configuration.yaml file and add the {integration_title} integration manually.",
|
||||
"title": "YAML import failed due to a connection error"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysaunum"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pysaunum==0.5.0"]
|
||||
"requirements": ["pysaunum==0.6.0"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ from . import DOMAIN
|
||||
class SceneActivatedTrigger(EntityTriggerBase):
|
||||
"""Trigger for scene entity activations."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
|
||||
@@ -17,13 +17,11 @@ from homeassistant.components import climate as FanState
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
PRESET_SLEEP,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
@@ -40,7 +38,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import DOMAIN, SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH
|
||||
from .const import (
|
||||
CLIMATE_PRESET_SCHEDULE,
|
||||
DOMAIN,
|
||||
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH,
|
||||
)
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
@@ -206,6 +208,7 @@ RADIATOR_PRESET_MODE_MAP: dict[str, SmartRadiatorThermostatMode] = {
|
||||
PRESET_BOOST: SmartRadiatorThermostatMode.FAST_HEATING,
|
||||
PRESET_COMFORT: SmartRadiatorThermostatMode.COMFORT,
|
||||
PRESET_HOME: SmartRadiatorThermostatMode.MANUAL,
|
||||
CLIMATE_PRESET_SCHEDULE: SmartRadiatorThermostatMode.SCHEDULE,
|
||||
}
|
||||
|
||||
RADIATOR_HA_PRESET_MODE_MAP = {
|
||||
@@ -227,15 +230,10 @@ class SwitchBotCloudSmartRadiatorThermostat(SwitchBotCloudEntity, ClimateEntity)
|
||||
_attr_target_temperature_step = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
_attr_preset_modes = [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_HOME,
|
||||
PRESET_SLEEP,
|
||||
]
|
||||
_attr_preset_modes = list(RADIATOR_PRESET_MODE_MAP)
|
||||
|
||||
_attr_translation_key = "smart_radiator_thermostat"
|
||||
|
||||
_attr_preset_mode = PRESET_HOME
|
||||
|
||||
_attr_hvac_modes = [
|
||||
@@ -300,7 +298,7 @@ class SwitchBotCloudSmartRadiatorThermostat(SwitchBotCloudEntity, ClimateEntity)
|
||||
SmartRadiatorThermostatMode(mode)
|
||||
]
|
||||
|
||||
if self.preset_mode in [PRESET_NONE, PRESET_AWAY]:
|
||||
if self.preset_mode == PRESET_NONE:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
|
||||
@@ -17,6 +17,9 @@ VACUUM_FAN_SPEED_STANDARD = "standard"
|
||||
VACUUM_FAN_SPEED_STRONG = "strong"
|
||||
VACUUM_FAN_SPEED_MAX = "max"
|
||||
|
||||
|
||||
CLIMATE_PRESET_SCHEDULE = "schedule"
|
||||
|
||||
AFTER_COMMAND_REFRESH = 5
|
||||
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
|
||||
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH = 30
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
"default": "mdi:chevron-left-box"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"smart_radiator_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"schedule": "mdi:clock-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"default": "mdi:air-purifier",
|
||||
|
||||
@@ -26,6 +26,17 @@
|
||||
"name": "Previous"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"smart_radiator_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"state_attributes": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiotankerkoenig"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotankerkoenig==0.4.2"]
|
||||
"requirements": ["aiotankerkoenig==0.5.1"]
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DESCRIPTION_PLACEHOLDERS: dict[str, str] = {
|
||||
"botfather_username": "@BotFather",
|
||||
"botfather_url": "https://t.me/botfather",
|
||||
"getidsbot_username": "@GetIDs Bot",
|
||||
"getidsbot_url": "https://t.me/getidsbot",
|
||||
"id_bot_username": "@id_bot",
|
||||
"id_bot_url": "https://t.me/id_bot",
|
||||
"socks_url": "socks5://username:password@proxy_ip:proxy_port",
|
||||
# used in advanced settings section
|
||||
"default_api_endpoint": DEFAULT_API_ENDPOINT,
|
||||
@@ -611,10 +611,15 @@ class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
errors["base"] = "chat_not_found"
|
||||
|
||||
service: TelegramNotificationService = self._get_entry().runtime_data
|
||||
description_placeholders = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
description_placeholders["bot_username"] = f"@{service.bot.username}"
|
||||
description_placeholders["bot_url"] = f"https://t.me/{service.bot.username}"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}),
|
||||
description_placeholders=DESCRIPTION_PLACEHOLDERS,
|
||||
description_placeholders=description_placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"data_description": {
|
||||
"chat_id": "ID representing the user or group chat to which messages can be sent."
|
||||
},
|
||||
"description": "To get your chat ID, follow these steps:\n\n1. Open Telegram and start a chat with [{getidsbot_username}]({getidsbot_url}).\n1. Send any message to the bot.\n1. Your chat ID is in the `id` field of the bot's response.",
|
||||
"description": "Before you proceed, send any message to your bot: [{bot_username}]({bot_url}). This is required because Telegram prevents bots from initiating chats with users.\n\nThen follow these steps to get your chat ID:\n\n1. Open Telegram and start a chat with [{id_bot_username}]({id_bot_url}).\n1. Send any message to the bot.\n1. Your chat ID is in the `ID` field of the bot's response.",
|
||||
"title": "Add chat"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ from .const import DOMAIN
|
||||
class TextChangedTrigger(EntityTriggerBase):
|
||||
"""Trigger for text entity when its content changes."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_domains = {DOMAIN}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["uhooapi==1.2.6"]
|
||||
"requirements": ["uhooapi==1.2.8"]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -63,7 +64,6 @@ SERVICE_STOP = "stop"
|
||||
DEFAULT_NAME = "Vacuum cleaner robot"
|
||||
|
||||
ISSUE_SEGMENTS_CHANGED = "segments_changed"
|
||||
ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED = "segments_mapping_not_configured"
|
||||
|
||||
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",)
|
||||
|
||||
@@ -438,7 +438,14 @@ class StateVacuumEntity(
|
||||
)
|
||||
|
||||
options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {})
|
||||
area_mapping: dict[str, list[str]] = options.get("area_mapping", {})
|
||||
area_mapping: dict[str, list[str]] | None = options.get("area_mapping")
|
||||
|
||||
if area_mapping is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="area_mapping_not_configured",
|
||||
translation_placeholders={"entity_id": self.entity_id},
|
||||
)
|
||||
|
||||
# We use a dict to preserve the order of segments.
|
||||
segment_ids: dict[str, None] = {}
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"area_mapping_not_configured": {
|
||||
"message": "Area mapping is not configured for `{entity_id}`. Configure the segment-to-area mapping before using this action."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"segments_changed": {
|
||||
"description": "",
|
||||
|
||||
@@ -35,9 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
and entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
):
|
||||
store: Store[list[dict[str, Any]]] = Store(hass, 1, DOMAIN)
|
||||
coordinator = VizioAppsDataUpdateCoordinator(hass, entry, store)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
coordinator = VizioAppsDataUpdateCoordinator(hass, store)
|
||||
await coordinator.async_setup()
|
||||
hass.data[DOMAIN][CONF_APPS] = coordinator
|
||||
await coordinator.async_refresh()
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -53,7 +54,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
):
|
||||
hass.data[DOMAIN].pop(CONF_APPS, None)
|
||||
if coordinator := hass.data[DOMAIN].pop(CONF_APPS, None):
|
||||
await coordinator.async_shutdown()
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any
|
||||
from pyvizio.const import APPS
|
||||
from pyvizio.util import gen_apps_list_from_url
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -23,19 +22,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
"""Define an object to hold Vizio app config data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
store: Store[list[dict[str, Any]]],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
config_entry=None,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(days=1),
|
||||
)
|
||||
@@ -43,8 +39,9 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]
|
||||
self.fail_threshold = 10
|
||||
self.store = store
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Refresh data for the first time when a config entry is setup."""
|
||||
async def async_setup(self) -> None:
|
||||
"""Load initial data from storage and register shutdown."""
|
||||
await self.async_register_shutdown()
|
||||
self.data = await self.store.async_load() or APPS
|
||||
|
||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -111,7 +111,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bo
|
||||
# Migrate unique_id from `xbox` to account xuid and
|
||||
# change generic entry name to user's gamertag
|
||||
try:
|
||||
own = await client.people.get_friends_by_xuid(client.xuid)
|
||||
own = await client.people.get_friend_by_xuid(client.xuid)
|
||||
except TimeoutException as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -74,7 +74,7 @@ class OAuth2FlowHandler(
|
||||
|
||||
client = XboxLiveClient(auth)
|
||||
|
||||
me = await client.people.get_friends_by_xuid(client.xuid)
|
||||
me = await client.people.get_friend_by_xuid(client.xuid)
|
||||
|
||||
await self.async_set_unique_id(client.xuid)
|
||||
|
||||
|
||||
@@ -213,10 +213,10 @@ class XboxPresenceCoordinator(XboxBaseCoordinator[XboxData]):
|
||||
async def update_data(self) -> XboxData:
|
||||
"""Fetch presence data."""
|
||||
|
||||
batch = await self.client.people.get_friends_by_xuid(self.client.xuid)
|
||||
me = await self.client.people.get_friend_by_xuid(self.client.xuid)
|
||||
friends = await self.client.people.get_friends_own()
|
||||
|
||||
presence_data = {self.client.xuid: batch.people[0]}
|
||||
presence_data = {self.client.xuid: me.people[0]}
|
||||
presence_data.update(
|
||||
{
|
||||
friend.xuid: friend
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
|
||||
"requirements": ["python-xbox==0.1.3"],
|
||||
"requirements": ["python-xbox==0.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Microsoft Corporation",
|
||||
|
||||
@@ -28,7 +28,7 @@ from .helpers import (
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
KEYS_TO_REDACT = {"homeId", "location"}
|
||||
KEYS_TO_REDACT = {"homeId", "location", "dsk"}
|
||||
|
||||
VALUES_TO_REDACT = (
|
||||
ZwaveValueMatcher(property_="userCode", command_class=CommandClass.USER_CODE),
|
||||
|
||||
@@ -381,3 +381,20 @@ class DependencyError(HomeAssistantError):
|
||||
f"Could not setup dependencies: {', '.join(failed_dependencies)}",
|
||||
)
|
||||
self.failed_dependencies = failed_dependencies
|
||||
|
||||
|
||||
class UnsupportedStorageVersionError(HomeAssistantError):
|
||||
"""Raised when a storage file has a newer major version than expected."""
|
||||
|
||||
def __init__(
|
||||
self, storage_key: str, found_version: int, max_supported_version: int
|
||||
) -> None:
|
||||
"""Initialize error."""
|
||||
super().__init__(
|
||||
f"Storage file {storage_key} has version {found_version}"
|
||||
f" which is newer than the max supported version {max_supported_version};"
|
||||
" upgrade Home Assistant or restore from a backup",
|
||||
)
|
||||
self.storage_key = storage_key
|
||||
self.found_version = found_version
|
||||
self.max_supported_version = max_supported_version
|
||||
|
||||
5
homeassistant/generated/bluetooth.py
generated
5
homeassistant/generated/bluetooth.py
generated
@@ -212,6 +212,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
||||
"domain": "govee_ble",
|
||||
"local_name": "GVH5110*",
|
||||
},
|
||||
{
|
||||
"connectable": False,
|
||||
"domain": "govee_ble",
|
||||
"local_name": "GV5140*",
|
||||
},
|
||||
{
|
||||
"connectable": False,
|
||||
"domain": "govee_ble",
|
||||
|
||||
@@ -7338,6 +7338,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ubisys": {
|
||||
"name": "Ubisys",
|
||||
"iot_standards": [
|
||||
"zigbee"
|
||||
]
|
||||
},
|
||||
"ubiwizz": {
|
||||
"name": "Ubiwizz",
|
||||
"integration_type": "virtual",
|
||||
|
||||
@@ -447,7 +447,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
|
||||
EventAreaRegistryUpdatedData(action="reorder", area_id=None),
|
||||
)
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the area registry."""
|
||||
self._async_setup_cleanup()
|
||||
|
||||
@@ -549,10 +549,10 @@ def async_get(hass: HomeAssistant) -> AreaRegistry:
|
||||
return AreaRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load area registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load()
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -77,7 +77,7 @@ class CategoryRegistryStore(Store[CategoryRegistryStoreData]):
|
||||
) -> CategoryRegistryStoreData:
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version > STORAGE_VERSION_MAJOR:
|
||||
raise ValueError("Can't migrate to future version")
|
||||
raise NotImplementedError
|
||||
|
||||
if old_major_version == 1:
|
||||
if old_minor_version < 2:
|
||||
@@ -204,7 +204,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]):
|
||||
|
||||
return new
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the category registry."""
|
||||
data = await self._store.async_load()
|
||||
category_entries: dict[str, dict[str, CategoryEntry]] = {}
|
||||
@@ -265,7 +265,7 @@ def async_get(hass: HomeAssistant) -> CategoryRegistry:
|
||||
return CategoryRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load category registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load()
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
@@ -1461,7 +1461,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
)
|
||||
self.async_schedule_save()
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the device registry."""
|
||||
async_setup_cleanup(self.hass, self)
|
||||
|
||||
@@ -1706,10 +1706,10 @@ def async_get(hass: HomeAssistant) -> DeviceRegistry:
|
||||
return DeviceRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load device registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load()
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -1678,7 +1678,7 @@ class EntityRegistry(BaseRegistry):
|
||||
new_options[domain] = options
|
||||
return self._async_update_entity(entity_id, options=new_options)
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the entity registry."""
|
||||
_async_setup_cleanup(self.hass, self)
|
||||
_async_setup_entity_restore(self.hass, self)
|
||||
@@ -1945,10 +1945,10 @@ def async_get(hass: HomeAssistant) -> EntityRegistry:
|
||||
return EntityRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load entity registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load()
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -94,7 +94,7 @@ class FloorRegistryStore(Store[FloorRegistryStoreData]):
|
||||
) -> FloorRegistryStoreData:
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version > STORAGE_VERSION_MAJOR:
|
||||
raise ValueError("Can't migrate to future version")
|
||||
raise NotImplementedError
|
||||
|
||||
if old_major_version == 1:
|
||||
if old_minor_version < 2:
|
||||
@@ -307,7 +307,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
|
||||
_EventFloorRegistryUpdatedData_Reorder(action="reorder"),
|
||||
)
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the floor registry."""
|
||||
data = await self._store.async_load()
|
||||
floors = FloorRegistryItems()
|
||||
@@ -353,7 +353,7 @@ def async_get(hass: HomeAssistant) -> FloorRegistry:
|
||||
return FloorRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load floor registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load()
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import importlib
|
||||
import logging
|
||||
import sys
|
||||
@@ -53,11 +52,10 @@ async def async_import_module(hass: HomeAssistant, name: str) -> ModuleType:
|
||||
if isinstance(ex, ModuleNotFoundError):
|
||||
failure_cache[name] = True
|
||||
import_future.set_exception(ex)
|
||||
with suppress(BaseException):
|
||||
# Set the exception retrieved flag on the future since
|
||||
# it will never be retrieved unless there
|
||||
# are concurrent calls
|
||||
import_future.result()
|
||||
# Set the exception retrieved flag on the future since
|
||||
# it will never be retrieved unless there
|
||||
# are concurrent calls
|
||||
import_future.exception()
|
||||
raise
|
||||
finally:
|
||||
del import_futures[name]
|
||||
|
||||
@@ -251,7 +251,7 @@ class IssueRegistry(BaseRegistry):
|
||||
"""
|
||||
self._store.make_read_only()
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the issue registry."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
@@ -314,12 +314,17 @@ def async_get(hass: HomeAssistant) -> IssueRegistry:
|
||||
return IssueRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant, *, read_only: bool = False) -> None:
|
||||
async def async_load(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
read_only: bool = False,
|
||||
load_empty: bool = False,
|
||||
) -> None:
|
||||
"""Load issue registry."""
|
||||
ir = async_get(hass)
|
||||
if read_only: # only used in for check config script
|
||||
ir.make_read_only()
|
||||
return await ir.async_load()
|
||||
await ir.async_load(load_empty=load_empty)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -80,7 +80,7 @@ class LabelRegistryStore(Store[LabelRegistryStoreData]):
|
||||
) -> LabelRegistryStoreData:
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version > STORAGE_VERSION_MAJOR:
|
||||
raise ValueError("Can't migrate to future version")
|
||||
raise NotImplementedError
|
||||
|
||||
if old_major_version == 1:
|
||||
if old_minor_version < 2:
|
||||
@@ -224,7 +224,7 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]):
|
||||
|
||||
return new
|
||||
|
||||
async def async_load(self) -> None:
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the label registry."""
|
||||
data = await self._store.async_load()
|
||||
labels = NormalizedNameBaseRegistryItems[LabelEntry]()
|
||||
@@ -270,7 +270,7 @@ def async_get(hass: HomeAssistant) -> LabelRegistry:
|
||||
return LabelRegistry(hass)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load label registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load()
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
@@ -77,6 +77,19 @@ class BaseRegistry[_StoreDataT: Mapping[str, Any] | Sequence[Any]](ABC):
|
||||
delay = SAVE_DELAY if self.hass.state is CoreState.running else SAVE_DELAY_LONG
|
||||
self._store.async_delay_save(self._data_to_save, delay)
|
||||
|
||||
async def async_load(self, *, load_empty: bool = False) -> None:
|
||||
"""Load the registry.
|
||||
|
||||
Optionally set the store to load empty and become read-only.
|
||||
"""
|
||||
if load_empty:
|
||||
self._store.set_load_empty()
|
||||
await self._async_load()
|
||||
|
||||
@abstractmethod
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the registry."""
|
||||
|
||||
@abstractmethod
|
||||
def _data_to_save(self) -> _StoreDataT:
|
||||
"""Return data of registry to store in a file."""
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Any, Self, cast
|
||||
|
||||
from homeassistant.const import ATTR_RESTORED, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, State, callback, valid_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.json import json_loads
|
||||
@@ -95,9 +95,12 @@ class StoredState:
|
||||
)
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant) -> None:
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load the restore state task."""
|
||||
await async_get(hass).async_setup()
|
||||
data = async_get(hass)
|
||||
if load_empty:
|
||||
data.set_load_empty()
|
||||
await data.async_setup()
|
||||
|
||||
|
||||
@callback
|
||||
@@ -124,6 +127,10 @@ class RestoreStateData:
|
||||
self.last_states: dict[str, StoredState] = {}
|
||||
self.entities: dict[str, RestoreEntity] = {}
|
||||
|
||||
def set_load_empty(self) -> None:
|
||||
"""Set the store to load empty and become read-only."""
|
||||
self.store.set_load_empty()
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up up the instance of this data helper."""
|
||||
await self.async_load()
|
||||
@@ -139,6 +146,8 @@ class RestoreStateData:
|
||||
"""Load the instance of this data helper."""
|
||||
try:
|
||||
stored_states = await self.store.async_load()
|
||||
except UnsupportedStorageVersionError:
|
||||
raise
|
||||
except HomeAssistantError as exc:
|
||||
_LOGGER.error("Error loading last states", exc_info=exc)
|
||||
stored_states = None
|
||||
|
||||
@@ -28,7 +28,7 @@ from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util, json as json_util
|
||||
from homeassistant.util.file import WriteError, write_utf8_file, write_utf8_file_atomic
|
||||
@@ -239,6 +239,7 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]:
|
||||
*,
|
||||
atomic_writes: bool = False,
|
||||
encoder: type[JSONEncoder] | None = None,
|
||||
max_readable_version: int | None = None,
|
||||
minor_version: int = 1,
|
||||
read_only: bool = False,
|
||||
serialize_in_event_loop: bool = True,
|
||||
@@ -246,6 +247,10 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]:
|
||||
"""Initialize storage class.
|
||||
|
||||
Args:
|
||||
max_readable_version: Maximum major version that can be read. Defaults
|
||||
to version. Set higher than version to support forward compatibility,
|
||||
allowing reading data written by newer versions (e.g., after downgrade).
|
||||
|
||||
serialize_in_event_loop: Whether to serialize data in the event loop.
|
||||
Set to True (default) if data passed to async_save and data produced by
|
||||
data_func passed to async_delay_save needs to be serialized in the event
|
||||
@@ -273,6 +278,10 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]:
|
||||
self._encoder = encoder
|
||||
self._atomic_writes = atomic_writes
|
||||
self._read_only = read_only
|
||||
self._load_empty = False
|
||||
self._max_readable_version = (
|
||||
max_readable_version if max_readable_version is not None else version
|
||||
)
|
||||
self._next_write_time = 0.0
|
||||
self._manager = get_internal_store_manager(hass)
|
||||
self._serialize_in_event_loop = serialize_in_event_loop
|
||||
@@ -289,6 +298,14 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]:
|
||||
"""
|
||||
self._read_only = True
|
||||
|
||||
def set_load_empty(self) -> None:
|
||||
"""Set the store to load empty data and become read-only.
|
||||
|
||||
When set, the store will skip loading data from disk and return None,
|
||||
while also becoming read-only to preserve on-disk data untouched.
|
||||
"""
|
||||
self._load_empty = True
|
||||
|
||||
async def async_load(self) -> _T | None:
|
||||
"""Load data.
|
||||
|
||||
@@ -328,6 +345,12 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]:
|
||||
|
||||
async def _async_load_data(self):
|
||||
"""Load the data."""
|
||||
# When load_empty is set, skip loading storage files and use empty
|
||||
# data while preserving the on-disk files untouched.
|
||||
if self._load_empty:
|
||||
self.make_read_only()
|
||||
return None
|
||||
|
||||
# Check if we have a pending write
|
||||
if self._data is not None:
|
||||
data = self._data
|
||||
@@ -415,6 +438,10 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]:
|
||||
):
|
||||
stored = data["data"]
|
||||
else:
|
||||
if data["version"] > self._max_readable_version:
|
||||
raise UnsupportedStorageVersionError(
|
||||
self.key, data["version"], self._max_readable_version
|
||||
)
|
||||
_LOGGER.info(
|
||||
"Migrating %s storage from %s.%s to %s.%s",
|
||||
self.key,
|
||||
|
||||
@@ -336,7 +336,7 @@ ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
class EntityTriggerBase(Trigger):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain: str
|
||||
_domains: set[str]
|
||||
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST
|
||||
|
||||
@override
|
||||
@@ -386,11 +386,11 @@ class EntityTriggerBase(Trigger):
|
||||
)
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
"""Filter entities of these domains."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == self._domain
|
||||
if split_entity_id(entity_id)[0] in self._domains
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -792,7 +792,7 @@ def make_entity_target_state_trigger(
|
||||
class CustomTrigger(EntityTargetStateTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_to_states = to_states_set
|
||||
|
||||
return CustomTrigger
|
||||
@@ -806,7 +806,7 @@ def make_entity_transition_trigger(
|
||||
class CustomTrigger(EntityTransitionTriggerBase):
|
||||
"""Trigger for conditional entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_from_states = from_states
|
||||
_to_states = to_states
|
||||
|
||||
@@ -821,7 +821,7 @@ def make_entity_origin_state_trigger(
|
||||
class CustomTrigger(EntityOriginStateTriggerBase):
|
||||
"""Trigger for entity "from state" changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_from_state = from_state
|
||||
|
||||
return CustomTrigger
|
||||
@@ -835,7 +835,7 @@ def make_entity_numerical_state_attribute_changed_trigger(
|
||||
class CustomTrigger(EntityNumericalStateAttributeChangedTriggerBase):
|
||||
"""Trigger for numerical state attribute changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_attribute = attribute
|
||||
|
||||
return CustomTrigger
|
||||
@@ -849,7 +849,7 @@ def make_entity_numerical_state_attribute_crossed_threshold_trigger(
|
||||
class CustomTrigger(EntityNumericalStateAttributeCrossedThresholdTriggerBase):
|
||||
"""Trigger for numerical state attribute changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_attribute = attribute
|
||||
|
||||
return CustomTrigger
|
||||
@@ -863,7 +863,7 @@ def make_entity_target_state_attribute_trigger(
|
||||
class CustomTrigger(EntityTargetStateAttributeTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_attribute = attribute
|
||||
_attribute_to_state = to_state
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ habluetooth==5.8.0
|
||||
hass-nabucasa==1.15.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260226.0
|
||||
home-assistant-frontend==20260302.0
|
||||
home-assistant-intents==2026.2.13
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
21
requirements_all.txt
generated
21
requirements_all.txt
generated
@@ -416,7 +416,7 @@ aioswitcher==6.1.0
|
||||
aiosyncthing==0.7.1
|
||||
|
||||
# homeassistant.components.tankerkoenig
|
||||
aiotankerkoenig==0.4.2
|
||||
aiotankerkoenig==0.5.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
aiotedee==0.2.25
|
||||
@@ -1223,7 +1223,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260226.0
|
||||
home-assistant-frontend==20260302.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.2.13
|
||||
@@ -1374,7 +1374,7 @@ kiwiki-client==0.1.1
|
||||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2026.2.25.165736
|
||||
knx-frontend==2026.3.2.183756
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@@ -1681,6 +1681,9 @@ onedrive-personal-sdk==0.1.4
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif_parsers==1.2.2
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.2.0
|
||||
|
||||
@@ -2233,7 +2236,7 @@ pylitejet==0.6.3
|
||||
pylitterbot==2025.1.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.26.0
|
||||
pylutron-caseta==0.27.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.18
|
||||
@@ -2427,7 +2430,7 @@ pysabnzbd==1.1.1
|
||||
pysaj==0.0.16
|
||||
|
||||
# homeassistant.components.saunum
|
||||
pysaunum==0.5.0
|
||||
pysaunum==0.6.0
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2025.9.0
|
||||
@@ -2605,7 +2608,7 @@ python-open-router==0.3.3
|
||||
python-opendata-transport==0.5.0
|
||||
|
||||
# homeassistant.components.openevse
|
||||
python-openevse-http==0.2.1
|
||||
python-openevse-http==0.2.5
|
||||
|
||||
# homeassistant.components.opensky
|
||||
python-opensky==1.0.1
|
||||
@@ -2657,7 +2660,7 @@ python-telegram-bot[socks]==22.1
|
||||
python-vlc==3.0.18122
|
||||
|
||||
# homeassistant.components.xbox
|
||||
python-xbox==0.1.3
|
||||
python-xbox==0.2.0
|
||||
|
||||
# homeassistant.components.egardia
|
||||
pythonegardia==1.0.52
|
||||
@@ -2790,7 +2793,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.5.5
|
||||
renault-api==0.5.6
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -3142,7 +3145,7 @@ typedmonarchmoney==0.7.0
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.uhoo
|
||||
uhooapi==1.2.6
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==10.2.2
|
||||
|
||||
21
requirements_test_all.txt
generated
21
requirements_test_all.txt
generated
@@ -401,7 +401,7 @@ aioswitcher==6.1.0
|
||||
aiosyncthing==0.7.1
|
||||
|
||||
# homeassistant.components.tankerkoenig
|
||||
aiotankerkoenig==0.4.2
|
||||
aiotankerkoenig==0.5.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
aiotedee==0.2.25
|
||||
@@ -1084,7 +1084,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260226.0
|
||||
home-assistant-frontend==20260302.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.2.13
|
||||
@@ -1211,7 +1211,7 @@ kegtron-ble==1.0.2
|
||||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2026.2.25.165736
|
||||
knx-frontend==2026.3.2.183756
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@@ -1467,6 +1467,9 @@ onedrive-personal-sdk==0.1.4
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif_parsers==1.2.2
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.2.0
|
||||
|
||||
@@ -1907,7 +1910,7 @@ pylitejet==0.6.3
|
||||
pylitterbot==2025.1.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.26.0
|
||||
pylutron-caseta==0.27.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.18
|
||||
@@ -2068,7 +2071,7 @@ pyrympro==0.0.9
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.saunum
|
||||
pysaunum==0.5.0
|
||||
pysaunum==0.6.0
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2025.9.0
|
||||
@@ -2204,7 +2207,7 @@ python-open-router==0.3.3
|
||||
python-opendata-transport==0.5.0
|
||||
|
||||
# homeassistant.components.openevse
|
||||
python-openevse-http==0.2.1
|
||||
python-openevse-http==0.2.5
|
||||
|
||||
# homeassistant.components.opensky
|
||||
python-opensky==1.0.1
|
||||
@@ -2250,7 +2253,7 @@ python-technove==2.0.0
|
||||
python-telegram-bot[socks]==22.1
|
||||
|
||||
# homeassistant.components.xbox
|
||||
python-xbox==0.1.3
|
||||
python-xbox==0.2.0
|
||||
|
||||
# homeassistant.components.uptime_kuma
|
||||
pythonkuma==0.5.0
|
||||
@@ -2362,7 +2365,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.5.5
|
||||
renault-api==0.5.6
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -2645,7 +2648,7 @@ typedmonarchmoney==0.7.0
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.uhoo
|
||||
uhooapi==1.2.6
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==10.2.2
|
||||
|
||||
@@ -2,5 +2,59 @@
|
||||
"tilt_eq": {
|
||||
"enabled": true,
|
||||
"intensity": 0
|
||||
},
|
||||
"user_eq": {
|
||||
"enabled": true,
|
||||
"bands": [
|
||||
{
|
||||
"index": 0,
|
||||
"filter": "LOWSHELF",
|
||||
"freq": 80,
|
||||
"gain": 0.0,
|
||||
"q": 0.8
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"filter": "PEAKING",
|
||||
"freq": 120,
|
||||
"gain": 0.0,
|
||||
"q": 1.24
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"filter": "PEAKING",
|
||||
"freq": 315,
|
||||
"gain": 0.0,
|
||||
"q": 1.24
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"filter": "PEAKING",
|
||||
"freq": 800,
|
||||
"gain": 0.0,
|
||||
"q": 1.24
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"filter": "PEAKING",
|
||||
"freq": 2000,
|
||||
"gain": 0.0,
|
||||
"q": 1.24
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"filter": "PEAKING",
|
||||
"freq": 5000,
|
||||
"gain": 0.0,
|
||||
"q": 1.24
|
||||
},
|
||||
{
|
||||
"index": 6,
|
||||
"filter": "HIGHSHELF",
|
||||
"freq": 8000,
|
||||
"gain": 0.0,
|
||||
"q": 0.8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,55 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[switch.cambridge_audio_cxnv2_equalizer-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.cambridge_audio_cxnv2_equalizer',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Equalizer',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Equalizer',
|
||||
'platform': 'cambridge_audio',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'equalizer',
|
||||
'unique_id': '0020c2d8-equalizer',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[switch.cambridge_audio_cxnv2_equalizer-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Cambridge Audio CXNv2 Equalizer',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.cambridge_audio_cxnv2_equalizer',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[switch.cambridge_audio_cxnv2_pre_amp-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -58,3 +58,62 @@ async def test_setting_value(
|
||||
blocking=True,
|
||||
)
|
||||
mock_stream_magic_client.set_early_update.assert_called_once_with(False)
|
||||
|
||||
|
||||
async def test_equalizer_switch(
|
||||
hass: HomeAssistant,
|
||||
mock_stream_magic_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test equalizer switch."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Test turning equalizer on
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "switch.cambridge_audio_cxnv2_equalizer",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_stream_magic_client.set_equalizer_mode.assert_called_once_with(True)
|
||||
mock_stream_magic_client.set_equalizer_mode.reset_mock()
|
||||
|
||||
# Test turning equalizer off
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: "switch.cambridge_audio_cxnv2_equalizer",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_stream_magic_client.set_equalizer_mode.assert_called_once_with(False)
|
||||
|
||||
|
||||
async def test_equalizer_switch_without_user_eq(
|
||||
hass: HomeAssistant,
|
||||
mock_stream_magic_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that equalizer switch entity is not created when user_eq is None."""
|
||||
# Set user_eq to None to simulate a device without EQ support
|
||||
mock_stream_magic_client.audio.user_eq = None
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Verify the equalizer switch entity was not created
|
||||
assert entity_registry.async_get("switch.cambridge_audio_cxnv2_equalizer") is None
|
||||
|
||||
# Verify other switch entities still exist
|
||||
assert entity_registry.async_get("switch.cambridge_audio_cxnv2_pre_amp") is not None
|
||||
assert (
|
||||
entity_registry.async_get("switch.cambridge_audio_cxnv2_early_update")
|
||||
is not None
|
||||
)
|
||||
assert (
|
||||
entity_registry.async_get("switch.cambridge_audio_cxnv2_room_correction")
|
||||
is not None
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.conversation import (
|
||||
async_get_chat_log,
|
||||
)
|
||||
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
|
||||
from homeassistant.components.conversation.models import ConversationResult
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -173,6 +174,36 @@ async def test_http_api_wrong_data(
|
||||
assert resp.status == HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
||||
async def test_http_processing_intent_with_device_satellite_ids(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test processing intent via HTTP API with both device_id and satellite_id."""
|
||||
client = await hass_client()
|
||||
mock_result = intent.IntentResponse(language=hass.config.language)
|
||||
mock_result.async_set_speech("test")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.http.async_converse",
|
||||
return_value=ConversationResult(response=mock_result),
|
||||
) as mock_converse:
|
||||
resp = await client.post(
|
||||
"/api/conversation/process",
|
||||
json={
|
||||
"text": "test",
|
||||
"device_id": "test-device-id",
|
||||
"satellite_id": "test-satellite-id",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
mock_converse.assert_called_once()
|
||||
call_kwargs = mock_converse.call_args[1]
|
||||
assert call_kwargs["device_id"] == "test-device-id"
|
||||
assert call_kwargs["satellite_id"] == "test-satellite-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
@@ -221,6 +252,38 @@ async def test_ws_api(
|
||||
assert msg["result"]["response"]["data"]["code"] == "no_intent_match"
|
||||
|
||||
|
||||
async def test_ws_api_with_device_satellite_ids(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the Websocket conversation API with both device_id and satellite_id."""
|
||||
client = await hass_ws_client(hass)
|
||||
mock_result = intent.IntentResponse(language=hass.config.language)
|
||||
mock_result.async_set_speech("test")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.http.async_converse",
|
||||
return_value=ConversationResult(response=mock_result),
|
||||
) as mock_converse:
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "conversation/process",
|
||||
"text": "test",
|
||||
"device_id": "test-device-id",
|
||||
"satellite_id": "test-satellite-id",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
mock_converse.assert_called_once()
|
||||
call_kwargs = mock_converse.call_args[1]
|
||||
assert call_kwargs["device_id"] == "test-device-id"
|
||||
assert call_kwargs["satellite_id"] == "test-satellite-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
|
||||
async def test_ws_prepare(
|
||||
hass: HomeAssistant, init_components, hass_ws_client: WebSocketGenerator, agent_id
|
||||
|
||||
@@ -1,19 +1,64 @@
|
||||
"""eafm fixtures."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.eafm.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_stations():
|
||||
def mock_get_stations() -> Generator[AsyncMock]:
|
||||
"""Mock aioeafm.get_stations."""
|
||||
with patch("homeassistant.components.eafm.config_flow.get_stations") as patched:
|
||||
patched.return_value = [
|
||||
{"label": "My station", "stationReference": "L12345", "RLOIid": "R12345"}
|
||||
]
|
||||
yield patched
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_station():
|
||||
def mock_get_station(initial_value: dict[str, Any]) -> Generator[AsyncMock]:
|
||||
"""Mock aioeafm.get_station."""
|
||||
with patch("homeassistant.components.eafm.coordinator.get_station") as patched:
|
||||
patched.return_value = initial_value
|
||||
yield patched
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def initial_value() -> dict[str, Any]:
|
||||
"""Mock aioeafm.get_station."""
|
||||
return {
|
||||
"label": "My station",
|
||||
"measures": [
|
||||
{
|
||||
"@id": "really-long-unique-id",
|
||||
"label": "York Viking Recorder - level-stage-i-15_min----",
|
||||
"qualifier": "Stage",
|
||||
"parameterName": "Water Level",
|
||||
"latestReading": {"value": 5},
|
||||
"stationReference": "L1234",
|
||||
"unit": "http://qudt.org/1.1/vocab/unit#Meter",
|
||||
"unitName": "m",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create a dummy config entry for testing."""
|
||||
entry = MockConfigEntry(
|
||||
version=1,
|
||||
domain=DOMAIN,
|
||||
entry_id="VikingRecorder1234",
|
||||
data={"station": "L1234"},
|
||||
title="Viking Recorder",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user