mirror of
https://github.com/home-assistant/core.git
synced 2025-08-02 18:18:21 +00:00
Merge branch 'dev' into homee-switch
This commit is contained in:
commit
ad3f17634f
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@ -324,7 +324,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
uses: sigstore/cosign-installer@v3.8.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -975,6 +975,7 @@ jobs:
|
|||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
-p no:sugar \
|
-p no:sugar \
|
||||||
|
--exclude-warning-annotations \
|
||||||
$(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \
|
$(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \
|
||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
@ -1098,6 +1099,7 @@ jobs:
|
|||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=10 \
|
--durations=10 \
|
||||||
-p no:sugar \
|
-p no:sugar \
|
||||||
|
--exclude-warning-annotations \
|
||||||
--dburl=mysql://root:password@127.0.0.1/homeassistant-test \
|
--dburl=mysql://root:password@127.0.0.1/homeassistant-test \
|
||||||
tests/components/history \
|
tests/components/history \
|
||||||
tests/components/logbook \
|
tests/components/logbook \
|
||||||
@ -1228,6 +1230,7 @@ jobs:
|
|||||||
--durations=0 \
|
--durations=0 \
|
||||||
--durations-min=10 \
|
--durations-min=10 \
|
||||||
-p no:sugar \
|
-p no:sugar \
|
||||||
|
--exclude-warning-annotations \
|
||||||
--dburl=postgresql://postgres:password@127.0.0.1/homeassistant-test \
|
--dburl=postgresql://postgres:password@127.0.0.1/homeassistant-test \
|
||||||
tests/components/history \
|
tests/components/history \
|
||||||
tests/components/logbook \
|
tests/components/logbook \
|
||||||
@ -1374,6 +1377,7 @@ jobs:
|
|||||||
--durations=0 \
|
--durations=0 \
|
||||||
--durations-min=1 \
|
--durations-min=1 \
|
||||||
-p no:sugar \
|
-p no:sugar \
|
||||||
|
--exclude-warning-annotations \
|
||||||
tests/components/${{ matrix.group }} \
|
tests/components/${{ matrix.group }} \
|
||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
|
@ -119,6 +119,7 @@ homeassistant.components.bluetooth_tracker.*
|
|||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
homeassistant.components.braviatv.*
|
homeassistant.components.braviatv.*
|
||||||
|
homeassistant.components.bring.*
|
||||||
homeassistant.components.brother.*
|
homeassistant.components.brother.*
|
||||||
homeassistant.components.browser.*
|
homeassistant.components.browser.*
|
||||||
homeassistant.components.bryant_evolution.*
|
homeassistant.components.bryant_evolution.*
|
||||||
|
@ -4,12 +4,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from airgradient import AirGradientClient
|
from airgradient import AirGradientClient
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientConfigEntry, AirGradientCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
@ -21,9 +20,6 @@ PLATFORMS: list[Platform] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
||||||
"""Set up Airgradient from a config entry."""
|
"""Set up Airgradient from a config entry."""
|
||||||
|
|
||||||
@ -31,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
|
|||||||
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirGradientCoordinator(hass, client)
|
coordinator = AirGradientCoordinator(hass, entry, client)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
@ -4,18 +4,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
|
||||||
from . import AirGradientConfigEntry
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -32,11 +31,17 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
|||||||
config_entry: AirGradientConfigEntry
|
config_entry: AirGradientConfigEntry
|
||||||
_current_version: str
|
_current_version: str
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirGradientConfigEntry,
|
||||||
|
client: AirGradientClient,
|
||||||
|
) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
logger=LOGGER,
|
logger=LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=f"AirGradient {client.host}",
|
name=f"AirGradient {client.host}",
|
||||||
update_interval=timedelta(minutes=1),
|
update_interval=timedelta(minutes=1),
|
||||||
)
|
)
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairq"],
|
"loggers": ["aioairq"],
|
||||||
"requirements": ["aioairq==0.4.3"]
|
"requirements": ["aioairq==0.4.4"]
|
||||||
}
|
}
|
||||||
|
@ -1531,7 +1531,7 @@ async def async_api_adjust_range(
|
|||||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
range_delta = directive.payload["rangeValueDelta"]
|
range_delta = directive.payload["rangeValueDelta"]
|
||||||
range_delta_default = bool(directive.payload["rangeValueDeltaDefault"])
|
range_delta_default = bool(directive.payload["rangeValueDeltaDefault"])
|
||||||
response_value: int | None = 0
|
response_value: float | None = 0
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
@ -387,4 +387,4 @@ def _validate_state_det_rules(state_det_rules: Any) -> list[Any] | None:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
_LOGGER.warning("Invalid state detection rules: %s", exc)
|
_LOGGER.warning("Invalid state detection rules: %s", exc)
|
||||||
return None
|
return None
|
||||||
return json_rules # type: ignore[no-any-return]
|
return json_rules
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"idle": "[%key:common::state::idle%]",
|
"idle": "[%key:common::state::idle%]",
|
||||||
"cook": "Cooking",
|
"cook": "Cooking",
|
||||||
"low_water": "Low water",
|
"low_water": "Low water",
|
||||||
"ota": "Ota",
|
"ota": "OTA update",
|
||||||
"provisioning": "Provisioning",
|
"provisioning": "Provisioning",
|
||||||
"high_temp": "High temperature",
|
"high_temp": "High temperature",
|
||||||
"device_failure": "Device failure"
|
"device_failure": "Device failure"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Assist Satellite intents."""
|
"""Assist Satellite intents."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -7,6 +9,8 @@ from homeassistant.helpers import entity_registry as er, intent
|
|||||||
|
|
||||||
from .const import DOMAIN, AssistSatelliteEntityFeature
|
from .const import DOMAIN, AssistSatelliteEntityFeature
|
||||||
|
|
||||||
|
EXCLUDED_DOMAINS: Final[set[str]] = {"voip"}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||||
"""Set up the intents."""
|
"""Set up the intents."""
|
||||||
@ -30,19 +34,36 @@ class BroadcastIntentHandler(intent.IntentHandler):
|
|||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
|
|
||||||
# Find all assist satellite entities that are not the one invoking the intent
|
# Find all assist satellite entities that are not the one invoking the intent
|
||||||
entities = {
|
entities: dict[str, er.RegistryEntry] = {}
|
||||||
entity: entry
|
for entity in hass.states.async_entity_ids(DOMAIN):
|
||||||
for entity in hass.states.async_entity_ids(DOMAIN)
|
entry = ent_reg.async_get(entity)
|
||||||
if (entry := ent_reg.async_get(entity))
|
if (
|
||||||
and entry.supported_features & AssistSatelliteEntityFeature.ANNOUNCE
|
(entry is None)
|
||||||
}
|
or (
|
||||||
|
# Supports announce
|
||||||
|
not (
|
||||||
|
entry.supported_features & AssistSatelliteEntityFeature.ANNOUNCE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Not the invoking device
|
||||||
|
or (intent_obj.device_id and (entry.device_id == intent_obj.device_id))
|
||||||
|
):
|
||||||
|
# Skip satellite
|
||||||
|
continue
|
||||||
|
|
||||||
if intent_obj.device_id:
|
# Check domain of config entry against excluded domains
|
||||||
entities = {
|
if (
|
||||||
entity: entry
|
entry.config_entry_id
|
||||||
for entity, entry in entities.items()
|
and (
|
||||||
if entry.device_id != intent_obj.device_id
|
config_entry := hass.config_entries.async_get_entry(
|
||||||
}
|
entry.config_entry_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
and (config_entry.domain in EXCLUDED_DOMAINS)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
entities[entity] = entry
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -54,7 +75,6 @@ class BroadcastIntentHandler(intent.IntentHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = intent_obj.create_response()
|
response = intent_obj.create_response()
|
||||||
response.async_set_speech("Done")
|
|
||||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||||
response.async_set_results(
|
response.async_set_results(
|
||||||
success_results=[
|
success_results=[
|
||||||
|
@ -9,6 +9,7 @@ from dataclasses import dataclass, replace
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
|
from itertools import chain
|
||||||
import json
|
import json
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
import shutil
|
import shutil
|
||||||
@ -827,7 +828,7 @@ class BackupManager:
|
|||||||
password=None,
|
password=None,
|
||||||
)
|
)
|
||||||
await written_backup.release_stream()
|
await written_backup.release_stream()
|
||||||
self.known_backups.add(written_backup.backup, agent_errors)
|
self.known_backups.add(written_backup.backup, agent_errors, [])
|
||||||
return written_backup.backup.backup_id
|
return written_backup.backup.backup_id
|
||||||
|
|
||||||
async def async_create_backup(
|
async def async_create_backup(
|
||||||
@ -951,12 +952,23 @@ class BackupManager:
|
|||||||
with_automatic_settings: bool,
|
with_automatic_settings: bool,
|
||||||
) -> NewBackup:
|
) -> NewBackup:
|
||||||
"""Initiate generating a backup."""
|
"""Initiate generating a backup."""
|
||||||
if not agent_ids:
|
unavailable_agents = [
|
||||||
raise BackupManagerError("At least one agent must be selected")
|
|
||||||
if invalid_agents := [
|
|
||||||
agent_id for agent_id in agent_ids if agent_id not in self.backup_agents
|
agent_id for agent_id in agent_ids if agent_id not in self.backup_agents
|
||||||
]:
|
]
|
||||||
raise BackupManagerError(f"Invalid agents selected: {invalid_agents}")
|
if not (
|
||||||
|
available_agents := [
|
||||||
|
agent_id for agent_id in agent_ids if agent_id in self.backup_agents
|
||||||
|
]
|
||||||
|
):
|
||||||
|
raise BackupManagerError(
|
||||||
|
f"At least one available backup agent must be selected, got {agent_ids}"
|
||||||
|
)
|
||||||
|
if unavailable_agents:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Backup agents %s are not available, will backupp to %s",
|
||||||
|
unavailable_agents,
|
||||||
|
available_agents,
|
||||||
|
)
|
||||||
if include_all_addons and include_addons:
|
if include_all_addons and include_addons:
|
||||||
raise BackupManagerError(
|
raise BackupManagerError(
|
||||||
"Cannot include all addons and specify specific addons"
|
"Cannot include all addons and specify specific addons"
|
||||||
@ -973,7 +985,7 @@ class BackupManager:
|
|||||||
new_backup,
|
new_backup,
|
||||||
self._backup_task,
|
self._backup_task,
|
||||||
) = await self._reader_writer.async_create_backup(
|
) = await self._reader_writer.async_create_backup(
|
||||||
agent_ids=agent_ids,
|
agent_ids=available_agents,
|
||||||
backup_name=backup_name,
|
backup_name=backup_name,
|
||||||
extra_metadata=extra_metadata
|
extra_metadata=extra_metadata
|
||||||
| {
|
| {
|
||||||
@ -992,7 +1004,9 @@ class BackupManager:
|
|||||||
raise BackupManagerError(str(err)) from err
|
raise BackupManagerError(str(err)) from err
|
||||||
|
|
||||||
backup_finish_task = self._backup_finish_task = self.hass.async_create_task(
|
backup_finish_task = self._backup_finish_task = self.hass.async_create_task(
|
||||||
self._async_finish_backup(agent_ids, with_automatic_settings, password),
|
self._async_finish_backup(
|
||||||
|
available_agents, unavailable_agents, with_automatic_settings, password
|
||||||
|
),
|
||||||
name="backup_manager_finish_backup",
|
name="backup_manager_finish_backup",
|
||||||
)
|
)
|
||||||
if not raise_task_error:
|
if not raise_task_error:
|
||||||
@ -1009,7 +1023,11 @@ class BackupManager:
|
|||||||
return new_backup
|
return new_backup
|
||||||
|
|
||||||
async def _async_finish_backup(
|
async def _async_finish_backup(
|
||||||
self, agent_ids: list[str], with_automatic_settings: bool, password: str | None
|
self,
|
||||||
|
available_agents: list[str],
|
||||||
|
unavailable_agents: list[str],
|
||||||
|
with_automatic_settings: bool,
|
||||||
|
password: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Finish a backup."""
|
"""Finish a backup."""
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -1028,7 +1046,7 @@ class BackupManager:
|
|||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Generated new backup with backup_id %s, uploading to agents %s",
|
"Generated new backup with backup_id %s, uploading to agents %s",
|
||||||
written_backup.backup.backup_id,
|
written_backup.backup.backup_id,
|
||||||
agent_ids,
|
available_agents,
|
||||||
)
|
)
|
||||||
self.async_on_backup_event(
|
self.async_on_backup_event(
|
||||||
CreateBackupEvent(
|
CreateBackupEvent(
|
||||||
@ -1041,13 +1059,15 @@ class BackupManager:
|
|||||||
try:
|
try:
|
||||||
agent_errors = await self._async_upload_backup(
|
agent_errors = await self._async_upload_backup(
|
||||||
backup=written_backup.backup,
|
backup=written_backup.backup,
|
||||||
agent_ids=agent_ids,
|
agent_ids=available_agents,
|
||||||
open_stream=written_backup.open_stream,
|
open_stream=written_backup.open_stream,
|
||||||
password=password,
|
password=password,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await written_backup.release_stream()
|
await written_backup.release_stream()
|
||||||
self.known_backups.add(written_backup.backup, agent_errors)
|
self.known_backups.add(
|
||||||
|
written_backup.backup, agent_errors, unavailable_agents
|
||||||
|
)
|
||||||
if not agent_errors:
|
if not agent_errors:
|
||||||
if with_automatic_settings:
|
if with_automatic_settings:
|
||||||
# create backup was successful, update last_completed_automatic_backup
|
# create backup was successful, update last_completed_automatic_backup
|
||||||
@ -1056,7 +1076,7 @@ class BackupManager:
|
|||||||
backup_success = True
|
backup_success = True
|
||||||
|
|
||||||
if with_automatic_settings:
|
if with_automatic_settings:
|
||||||
self._update_issue_after_agent_upload(agent_errors)
|
self._update_issue_after_agent_upload(agent_errors, unavailable_agents)
|
||||||
# delete old backups more numerous than copies
|
# delete old backups more numerous than copies
|
||||||
# try this regardless of agent errors above
|
# try this regardless of agent errors above
|
||||||
await delete_backups_exceeding_configured_count(self)
|
await delete_backups_exceeding_configured_count(self)
|
||||||
@ -1216,10 +1236,10 @@ class BackupManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _update_issue_after_agent_upload(
|
def _update_issue_after_agent_upload(
|
||||||
self, agent_errors: dict[str, Exception]
|
self, agent_errors: dict[str, Exception], unavailable_agents: list[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update issue registry after a backup is uploaded to agents."""
|
"""Update issue registry after a backup is uploaded to agents."""
|
||||||
if not agent_errors:
|
if not agent_errors and not unavailable_agents:
|
||||||
ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed")
|
ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed")
|
||||||
return
|
return
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
@ -1233,7 +1253,13 @@ class BackupManager:
|
|||||||
translation_key="automatic_backup_failed_upload_agents",
|
translation_key="automatic_backup_failed_upload_agents",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"failed_agents": ", ".join(
|
"failed_agents": ", ".join(
|
||||||
self.backup_agents[agent_id].name for agent_id in agent_errors
|
chain(
|
||||||
|
(
|
||||||
|
self.backup_agents[agent_id].name
|
||||||
|
for agent_id in agent_errors
|
||||||
|
),
|
||||||
|
unavailable_agents,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1302,11 +1328,12 @@ class KnownBackups:
|
|||||||
self,
|
self,
|
||||||
backup: AgentBackup,
|
backup: AgentBackup,
|
||||||
agent_errors: dict[str, Exception],
|
agent_errors: dict[str, Exception],
|
||||||
|
unavailable_agents: list[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a backup."""
|
"""Add a backup."""
|
||||||
self._backups[backup.backup_id] = KnownBackup(
|
self._backups[backup.backup_id] = KnownBackup(
|
||||||
backup_id=backup.backup_id,
|
backup_id=backup.backup_id,
|
||||||
failed_agent_ids=list(agent_errors),
|
failed_agent_ids=list(chain(agent_errors, unavailable_agents)),
|
||||||
)
|
)
|
||||||
self._manager.store.save()
|
self._manager.store.save()
|
||||||
|
|
||||||
@ -1412,7 +1439,11 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
|||||||
manager = self._hass.data[DATA_MANAGER]
|
manager = self._hass.data[DATA_MANAGER]
|
||||||
|
|
||||||
agent_config = manager.config.data.agents.get(self._local_agent_id)
|
agent_config = manager.config.data.agents.get(self._local_agent_id)
|
||||||
if agent_config and not agent_config.protected:
|
if (
|
||||||
|
self._local_agent_id in agent_ids
|
||||||
|
and agent_config
|
||||||
|
and not agent_config.protected
|
||||||
|
):
|
||||||
password = None
|
password = None
|
||||||
|
|
||||||
backup = AgentBackup(
|
backup = AgentBackup(
|
||||||
|
@ -122,7 +122,7 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
|||||||
def suggested_filename_from_name_date(name: str, date_str: str) -> str:
|
def suggested_filename_from_name_date(name: str, date_str: str) -> str:
|
||||||
"""Suggest a filename for the backup."""
|
"""Suggest a filename for the backup."""
|
||||||
date = dt_util.parse_datetime(date_str, raise_on_error=True)
|
date = dt_util.parse_datetime(date_str, raise_on_error=True)
|
||||||
return "_".join(f"{name} - {date.strftime('%Y-%m-%d %H.%M %S%f')}.tar".split())
|
return "_".join(f"{name} {date.strftime('%Y-%m-%d %H.%M %S%f')}.tar".split())
|
||||||
|
|
||||||
|
|
||||||
def suggested_filename(backup: AgentBackup) -> str:
|
def suggested_filename(backup: AgentBackup) -> str:
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
"""The bluesound component."""
|
"""The bluesound component."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from pyblu import Player
|
||||||
|
|
||||||
from pyblu import Player, SyncStatus
|
|
||||||
from pyblu.errors import PlayerUnreachableError
|
from pyblu.errors import PlayerUnreachableError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -14,7 +12,11 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import BluesoundCoordinator
|
from .coordinator import (
|
||||||
|
BluesoundConfigEntry,
|
||||||
|
BluesoundCoordinator,
|
||||||
|
BluesoundRuntimeData,
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
@ -23,18 +25,6 @@ PLATFORMS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BluesoundRuntimeData:
|
|
||||||
"""Bluesound data class."""
|
|
||||||
|
|
||||||
player: Player
|
|
||||||
sync_status: SyncStatus
|
|
||||||
coordinator: BluesoundCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the Bluesound."""
|
"""Set up the Bluesound."""
|
||||||
return True
|
return True
|
||||||
@ -53,7 +43,7 @@ async def async_setup_entry(
|
|||||||
except PlayerUnreachableError as ex:
|
except PlayerUnreachableError as ex:
|
||||||
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
|
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
|
||||||
|
|
||||||
coordinator = BluesoundCoordinator(hass, player, sync_status)
|
coordinator = BluesoundCoordinator(hass, config_entry, player, sync_status)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status, coordinator)
|
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status, coordinator)
|
||||||
|
@ -12,6 +12,7 @@ import logging
|
|||||||
from pyblu import Input, Player, Preset, Status, SyncStatus
|
from pyblu import Input, Player, Preset, Status, SyncStatus
|
||||||
from pyblu.errors import PlayerUnreachableError
|
from pyblu.errors import PlayerUnreachableError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
@ -21,6 +22,15 @@ NODE_OFFLINE_CHECK_TIMEOUT = timedelta(minutes=3)
|
|||||||
PRESET_AND_INPUTS_INTERVAL = timedelta(minutes=15)
|
PRESET_AND_INPUTS_INTERVAL = timedelta(minutes=15)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BluesoundRuntimeData:
|
||||||
|
"""Bluesound data class."""
|
||||||
|
|
||||||
|
player: Player
|
||||||
|
sync_status: SyncStatus
|
||||||
|
coordinator: BluesoundCoordinator
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BluesoundData:
|
class BluesoundData:
|
||||||
"""Define a class to hold Bluesound data."""
|
"""Define a class to hold Bluesound data."""
|
||||||
@ -31,6 +41,9 @@ class BluesoundData:
|
|||||||
inputs: list[Input]
|
inputs: list[Input]
|
||||||
|
|
||||||
|
|
||||||
|
type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
|
||||||
|
|
||||||
|
|
||||||
def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]:
|
def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]:
|
||||||
"""Cancel a task."""
|
"""Cancel a task."""
|
||||||
|
|
||||||
@ -45,8 +58,14 @@ def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]
|
|||||||
class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]):
|
class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]):
|
||||||
"""Define an object to hold Bluesound data."""
|
"""Define an object to hold Bluesound data."""
|
||||||
|
|
||||||
|
config_entry: BluesoundConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, player: Player, sync_status: SyncStatus
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BluesoundConfigEntry,
|
||||||
|
player: Player,
|
||||||
|
sync_status: SyncStatus,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.player = player
|
self.player = player
|
||||||
@ -55,12 +74,11 @@ class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
logger=_LOGGER,
|
logger=_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=sync_status.name,
|
name=sync_status.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_setup(self) -> None:
|
||||||
assert self.config_entry is not None
|
|
||||||
|
|
||||||
preset = await self.player.presets()
|
preset = await self.player.presets()
|
||||||
inputs = await self.player.inputs()
|
inputs = await self.player.inputs()
|
||||||
status = await self.player.status()
|
status = await self.player.status()
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"bluetooth-adapters==0.21.4",
|
"bluetooth-adapters==0.21.4",
|
||||||
"bluetooth-auto-recovery==1.4.2",
|
"bluetooth-auto-recovery==1.4.2",
|
||||||
"bluetooth-data-tools==1.23.4",
|
"bluetooth-data-tools==1.23.4",
|
||||||
"dbus-fast==2.32.0",
|
"dbus-fast==2.33.0",
|
||||||
"habluetooth==3.21.1"
|
"habluetooth==3.21.1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,19 +6,16 @@ import logging
|
|||||||
|
|
||||||
from bring_api import Bring
|
from bring_api import Bring
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .coordinator import BringDataUpdateCoordinator
|
from .coordinator import BringConfigEntry, BringDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.TODO]
|
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
|
||||||
"""Set up Bring! from a config entry."""
|
"""Set up Bring! from a config entry."""
|
||||||
@ -26,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo
|
|||||||
session = async_get_clientsession(hass)
|
session = async_get_clientsession(hass)
|
||||||
bring = Bring(session, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
bring = Bring(session, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
||||||
|
|
||||||
coordinator = BringDataUpdateCoordinator(hass, bring)
|
coordinator = BringDataUpdateCoordinator(hass, entry, bring)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
@ -36,6 +33,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
@ -68,7 +68,13 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user",
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"google_play": "https://play.google.com/store/apps/details?id=ch.publisheria.bring",
|
||||||
|
"app_store": "https://itunes.apple.com/app/apple-store/id580669177",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
@ -101,6 +107,29 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reconfiguration of the integration."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
reconf_entry = self._get_reconfigure_entry()
|
||||||
|
|
||||||
|
if user_input:
|
||||||
|
if not (errors := await self.validate_input(user_input)):
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
reconf_entry, data_updates=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reconfigure",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
suggested_values={CONF_EMAIL: reconf_entry.data[CONF_EMAIL]},
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
async def validate_input(self, user_input: Mapping[str, Any]) -> dict[str, str]:
|
async def validate_input(self, user_input: Mapping[str, Any]) -> dict[str, str]:
|
||||||
"""Auth Helper."""
|
"""Auth Helper."""
|
||||||
|
|
||||||
|
@ -8,11 +8,15 @@ import logging
|
|||||||
|
|
||||||
from bring_api import (
|
from bring_api import (
|
||||||
Bring,
|
Bring,
|
||||||
|
BringActivityResponse,
|
||||||
BringAuthException,
|
BringAuthException,
|
||||||
|
BringItemsResponse,
|
||||||
|
BringList,
|
||||||
BringParseException,
|
BringParseException,
|
||||||
BringRequestException,
|
BringRequestException,
|
||||||
|
BringUserSettingsResponse,
|
||||||
|
BringUsersResponse,
|
||||||
)
|
)
|
||||||
from bring_api.types import BringItemsResponse, BringList, BringUserSettingsResponse
|
|
||||||
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -26,6 +30,8 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BringData(DataClassORJSONMixin):
|
class BringData(DataClassORJSONMixin):
|
||||||
@ -33,20 +39,25 @@ class BringData(DataClassORJSONMixin):
|
|||||||
|
|
||||||
lst: BringList
|
lst: BringList
|
||||||
content: BringItemsResponse
|
content: BringItemsResponse
|
||||||
|
activity: BringActivityResponse
|
||||||
|
users: BringUsersResponse
|
||||||
|
|
||||||
|
|
||||||
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||||
"""A Bring Data Update Coordinator."""
|
"""A Bring Data Update Coordinator."""
|
||||||
|
|
||||||
config_entry: ConfigEntry
|
config_entry: BringConfigEntry
|
||||||
user_settings: BringUserSettingsResponse
|
user_settings: BringUserSettingsResponse
|
||||||
lists: list[BringList]
|
lists: list[BringList]
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, bring: Bring) -> None:
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring
|
||||||
|
) -> None:
|
||||||
"""Initialize the Bring data coordinator."""
|
"""Initialize the Bring data coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=90),
|
update_interval=timedelta(seconds=90),
|
||||||
)
|
)
|
||||||
@ -59,23 +70,21 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
|||||||
try:
|
try:
|
||||||
self.lists = (await self.bring.load_lists()).lists
|
self.lists = (await self.bring.load_lists()).lists
|
||||||
except BringRequestException as e:
|
except BringRequestException as e:
|
||||||
raise UpdateFailed("Unable to connect and retrieve data from bring") from e
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="setup_request_exception",
|
||||||
|
) from e
|
||||||
except BringParseException as e:
|
except BringParseException as e:
|
||||||
raise UpdateFailed("Unable to parse response from bring") from e
|
raise UpdateFailed(
|
||||||
except BringAuthException:
|
translation_domain=DOMAIN,
|
||||||
# try to recover by refreshing access token, otherwise
|
translation_key="setup_parse_exception",
|
||||||
# initiate reauth flow
|
) from e
|
||||||
try:
|
except BringAuthException as e:
|
||||||
await self.bring.retrieve_new_access_token()
|
raise ConfigEntryAuthFailed(
|
||||||
except (BringRequestException, BringParseException) as exc:
|
translation_domain=DOMAIN,
|
||||||
raise UpdateFailed("Refreshing authentication token failed") from exc
|
translation_key="setup_authentication_exception",
|
||||||
except BringAuthException as exc:
|
translation_placeholders={CONF_EMAIL: self.bring.mail},
|
||||||
raise ConfigEntryAuthFailed(
|
) from e
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="setup_authentication_exception",
|
|
||||||
translation_placeholders={CONF_EMAIL: self.bring.mail},
|
|
||||||
) from exc
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
if self.previous_lists - (
|
if self.previous_lists - (
|
||||||
current_lists := {lst.listUuid for lst in self.lists}
|
current_lists := {lst.listUuid for lst in self.lists}
|
||||||
@ -89,14 +98,20 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
items = await self.bring.get_list(lst.listUuid)
|
items = await self.bring.get_list(lst.listUuid)
|
||||||
|
activity = await self.bring.get_activity(lst.listUuid)
|
||||||
|
users = await self.bring.get_list_users(lst.listUuid)
|
||||||
except BringRequestException as e:
|
except BringRequestException as e:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
"Unable to connect and retrieve data from bring"
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="setup_request_exception",
|
||||||
) from e
|
) from e
|
||||||
except BringParseException as e:
|
except BringParseException as e:
|
||||||
raise UpdateFailed("Unable to parse response from bring") from e
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="setup_parse_exception",
|
||||||
|
) from e
|
||||||
else:
|
else:
|
||||||
list_dict[lst.listUuid] = BringData(lst, items)
|
list_dict[lst.listUuid] = BringData(lst, items, activity, users)
|
||||||
|
|
||||||
return list_dict
|
return list_dict
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import BringConfigEntry
|
from .coordinator import BringConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
@ -14,4 +14,8 @@ async def async_get_config_entry_diagnostics(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
return {k: v.to_dict() for k, v in config_entry.runtime_data.data.items()}
|
return {
|
||||||
|
"data": {k: v.to_dict() for k, v in config_entry.runtime_data.data.items()},
|
||||||
|
"lists": [lst.to_dict() for lst in config_entry.runtime_data.lists],
|
||||||
|
"user_settings": config_entry.runtime_data.user_settings.to_dict(),
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from bring_api.types import BringList
|
from bring_api import BringList
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
108
homeassistant/components/bring/event.py
Normal file
108
homeassistant/components/bring/event.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""Event platform for Bring integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from bring_api import ActivityType, BringList
|
||||||
|
|
||||||
|
from homeassistant.components.event import EventEntity
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import BringConfigEntry
|
||||||
|
from .coordinator import BringDataUpdateCoordinator
|
||||||
|
from .entity import BringBaseEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BringConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the event platform."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
lists_added: set[str] = set()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def add_entities() -> None:
|
||||||
|
"""Add event entities."""
|
||||||
|
nonlocal lists_added
|
||||||
|
|
||||||
|
if new_lists := {lst.listUuid for lst in coordinator.lists} - lists_added:
|
||||||
|
async_add_entities(
|
||||||
|
BringEventEntity(
|
||||||
|
coordinator,
|
||||||
|
bring_list,
|
||||||
|
)
|
||||||
|
for bring_list in coordinator.lists
|
||||||
|
if bring_list.listUuid in new_lists
|
||||||
|
)
|
||||||
|
lists_added |= new_lists
|
||||||
|
|
||||||
|
coordinator.async_add_listener(add_entities)
|
||||||
|
add_entities()
|
||||||
|
|
||||||
|
|
||||||
|
class BringEventEntity(BringBaseEntity, EventEntity):
|
||||||
|
"""An event entity."""
|
||||||
|
|
||||||
|
_attr_translation_key = "activities"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BringDataUpdateCoordinator,
|
||||||
|
bring_list: BringList,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator, bring_list)
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{coordinator.config_entry.unique_id}_{self._list_uuid}_activities"
|
||||||
|
)
|
||||||
|
self._attr_event_types = [event.name.lower() for event in ActivityType]
|
||||||
|
|
||||||
|
def _async_handle_event(self) -> None:
|
||||||
|
"""Handle the activity event."""
|
||||||
|
bring_list = self.coordinator.data[self._list_uuid]
|
||||||
|
last_event_triggered = self.state
|
||||||
|
if bring_list.activity.timeline and (
|
||||||
|
last_event_triggered is None
|
||||||
|
or datetime.fromisoformat(last_event_triggered)
|
||||||
|
< bring_list.activity.timestamp
|
||||||
|
):
|
||||||
|
activity = bring_list.activity.timeline[0]
|
||||||
|
attributes = asdict(activity.content)
|
||||||
|
|
||||||
|
attributes["last_activity_by"] = next(
|
||||||
|
x.name
|
||||||
|
for x in bring_list.users.users
|
||||||
|
if x.publicUuid == activity.content.publicUserUuid
|
||||||
|
)
|
||||||
|
|
||||||
|
self._trigger_event(
|
||||||
|
activity.type.name.lower(),
|
||||||
|
attributes,
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_picture(self) -> str | None:
|
||||||
|
"""Return the entity picture to use in the frontend, if any."""
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"https://api.getbring.com/rest/v2/bringusers/profilepictures/{public_uuid}"
|
||||||
|
if (public_uuid := self.state_attributes.get("publicUserUuid"))
|
||||||
|
else super().entity_picture
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Register callbacks with your device API/library."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._async_handle_event()
|
||||||
|
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
self._async_handle_event()
|
||||||
|
return super()._handle_coordinator_update()
|
@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"event": {
|
||||||
|
"activity": {
|
||||||
|
"default": "mdi:bell"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"urgent": {
|
"urgent": {
|
||||||
"default": "mdi:run-fast"
|
"default": "mdi:run-fast"
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["bring_api"],
|
"loggers": ["bring_api"],
|
||||||
"requirements": ["bring-api==1.0.0"]
|
"requirements": ["bring-api==1.0.1"]
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ rules:
|
|||||||
brands: done
|
brands: done
|
||||||
common-modules: done
|
common-modules: done
|
||||||
config-flow-test-coverage: done
|
config-flow-test-coverage: done
|
||||||
config-flow: todo
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
docs-actions: done
|
docs-actions: done
|
||||||
docs-high-level-description: todo
|
docs-high-level-description: todo
|
||||||
@ -58,9 +58,9 @@ rules:
|
|||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: todo
|
exception-translations: done
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: done
|
||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
@ -69,4 +69,4 @@ rules:
|
|||||||
# Platinum
|
# Platinum
|
||||||
async-dependency: done
|
async-dependency: done
|
||||||
inject-websession: done
|
inject-websession: done
|
||||||
strict-typing: todo
|
strict-typing: done
|
||||||
|
@ -6,9 +6,8 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from bring_api import BringUserSettingsResponse
|
from bring_api import BringList, BringUserSettingsResponse
|
||||||
from bring_api.const import BRING_SUPPORTED_LOCALES
|
from bring_api.const import BRING_SUPPORTED_LOCALES
|
||||||
from bring_api.types import BringList
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -20,8 +19,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from . import BringConfigEntry
|
from .coordinator import BringConfigEntry, BringData, BringDataUpdateCoordinator
|
||||||
from .coordinator import BringData, BringDataUpdateCoordinator
|
|
||||||
from .entity import BringBaseEntity
|
from .entity import BringBaseEntity
|
||||||
from .util import list_language, sum_attributes
|
from .util import list_language, sum_attributes
|
||||||
|
|
||||||
|
@ -5,9 +5,15 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "Bring! Grocery shopping list",
|
||||||
|
"description": "Connect your Bring! account to sync your shopping lists with Home Assistant.\n\nDon't have a Bring! account? Download the app on [Google Play for Android]({google_play}) or the [App Store for iOS]({app_store}) to sign up.",
|
||||||
"data": {
|
"data": {
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"email": "The email address associated with your Bring! account.",
|
||||||
|
"password": "The password to login to your Bring! account."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
@ -16,21 +22,53 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"email": "[%key:component::bring::config::step::user::data_description::email%]",
|
||||||
|
"password": "[%key:component::bring::config::step::user::data_description::email%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reconfigure": {
|
||||||
|
"title": "Bring! configuration",
|
||||||
|
"description": "Update your credentials if you have changed your Bring! account email or password.",
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"email": "[%key:component::bring::config::step::user::data_description::email%]",
|
||||||
|
"password": "[%key:component::bring::config::step::user::data_description::email%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||||
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"unique_id_mismatch": "The login details correspond to a different account. Please re-authenticate to the previously configured account."
|
"unique_id_mismatch": "The login details correspond to a different account. Please re-authenticate to the previously configured account.",
|
||||||
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"event": {
|
||||||
|
"activities": {
|
||||||
|
"name": "Activities",
|
||||||
|
"state_attributes": {
|
||||||
|
"event_type": {
|
||||||
|
"state": {
|
||||||
|
"list_items_added": "Items added",
|
||||||
|
"list_items_changed": "Items changed",
|
||||||
|
"list_items_removed": "Items removed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"urgent": {
|
"urgent": {
|
||||||
"name": "Urgent",
|
"name": "Urgent",
|
||||||
|
@ -9,10 +9,10 @@ import uuid
|
|||||||
from bring_api import (
|
from bring_api import (
|
||||||
BringItem,
|
BringItem,
|
||||||
BringItemOperation,
|
BringItemOperation,
|
||||||
|
BringList,
|
||||||
BringNotificationType,
|
BringNotificationType,
|
||||||
BringRequestException,
|
BringRequestException,
|
||||||
)
|
)
|
||||||
from bring_api.types import BringList
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
@ -26,14 +26,13 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import BringConfigEntry
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ITEM_NAME,
|
ATTR_ITEM_NAME,
|
||||||
ATTR_NOTIFICATION_TYPE,
|
ATTR_NOTIFICATION_TYPE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_PUSH_NOTIFICATION,
|
SERVICE_PUSH_NOTIFICATION,
|
||||||
)
|
)
|
||||||
from .coordinator import BringData, BringDataUpdateCoordinator
|
from .coordinator import BringConfigEntry, BringData, BringDataUpdateCoordinator
|
||||||
from .entity import BringBaseEntity
|
from .entity import BringBaseEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
@ -174,7 +174,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str | None,
|
||||||
entity_id: str,
|
entity_id: str,
|
||||||
coordinator: CalDavUpdateCoordinator,
|
coordinator: CalDavUpdateCoordinator,
|
||||||
unique_id: str | None = None,
|
unique_id: str | None = None,
|
||||||
|
@ -8,16 +8,12 @@ from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
from aiohttp import ClientError, ClientTimeout
|
from aiohttp import ClientError
|
||||||
from hass_nabucasa import Cloud, CloudError
|
from hass_nabucasa import Cloud, CloudError
|
||||||
from hass_nabucasa.cloud_api import (
|
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||||
async_files_delete_file,
|
from hass_nabucasa.cloud_api import async_files_delete_file, async_files_list
|
||||||
async_files_download_details,
|
|
||||||
async_files_list,
|
|
||||||
async_files_upload_details,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -28,7 +24,7 @@ from .client import CloudClient
|
|||||||
from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT
|
from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_STORAGE_BACKUP = "backup"
|
_STORAGE_BACKUP: Literal["backup"] = "backup"
|
||||||
_RETRY_LIMIT = 5
|
_RETRY_LIMIT = 5
|
||||||
_RETRY_SECONDS_MIN = 60
|
_RETRY_SECONDS_MIN = 60
|
||||||
_RETRY_SECONDS_MAX = 600
|
_RETRY_SECONDS_MAX = 600
|
||||||
@ -109,63 +105,14 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
raise BackupAgentError("Backup not found")
|
raise BackupAgentError("Backup not found")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
details = await async_files_download_details(
|
content = await self._cloud.files.download(
|
||||||
self._cloud,
|
|
||||||
storage_type=_STORAGE_BACKUP,
|
storage_type=_STORAGE_BACKUP,
|
||||||
filename=self._get_backup_filename(),
|
filename=self._get_backup_filename(),
|
||||||
)
|
)
|
||||||
except (ClientError, CloudError) as err:
|
except CloudError as err:
|
||||||
raise BackupAgentError("Failed to get download details") from err
|
raise BackupAgentError(f"Failed to download backup: {err}") from err
|
||||||
|
|
||||||
try:
|
return ChunkAsyncStreamIterator(content)
|
||||||
resp = await self._cloud.websession.get(
|
|
||||||
details["url"],
|
|
||||||
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
|
|
||||||
)
|
|
||||||
|
|
||||||
resp.raise_for_status()
|
|
||||||
except ClientError as err:
|
|
||||||
raise BackupAgentError("Failed to download backup") from err
|
|
||||||
|
|
||||||
return ChunkAsyncStreamIterator(resp.content)
|
|
||||||
|
|
||||||
async def _async_do_upload_backup(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
|
||||||
filename: str,
|
|
||||||
base64md5hash: str,
|
|
||||||
metadata: dict[str, Any],
|
|
||||||
size: int,
|
|
||||||
) -> None:
|
|
||||||
"""Upload a backup."""
|
|
||||||
try:
|
|
||||||
details = await async_files_upload_details(
|
|
||||||
self._cloud,
|
|
||||||
storage_type=_STORAGE_BACKUP,
|
|
||||||
filename=filename,
|
|
||||||
metadata=metadata,
|
|
||||||
size=size,
|
|
||||||
base64md5hash=base64md5hash,
|
|
||||||
)
|
|
||||||
except (ClientError, CloudError) as err:
|
|
||||||
raise BackupAgentError("Failed to get upload details") from err
|
|
||||||
|
|
||||||
try:
|
|
||||||
upload_status = await self._cloud.websession.put(
|
|
||||||
details["url"],
|
|
||||||
data=await open_stream(),
|
|
||||||
headers=details["headers"] | {"content-length": str(size)},
|
|
||||||
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
|
|
||||||
)
|
|
||||||
_LOGGER.log(
|
|
||||||
logging.DEBUG if upload_status.status < 400 else logging.WARNING,
|
|
||||||
"Backup upload status: %s",
|
|
||||||
upload_status.status,
|
|
||||||
)
|
|
||||||
upload_status.raise_for_status()
|
|
||||||
except (TimeoutError, ClientError) as err:
|
|
||||||
raise BackupAgentError("Failed to upload backup") from err
|
|
||||||
|
|
||||||
async def async_upload_backup(
|
async def async_upload_backup(
|
||||||
self,
|
self,
|
||||||
@ -190,7 +137,8 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
tries = 1
|
tries = 1
|
||||||
while tries <= _RETRY_LIMIT:
|
while tries <= _RETRY_LIMIT:
|
||||||
try:
|
try:
|
||||||
await self._async_do_upload_backup(
|
await self._cloud.files.upload(
|
||||||
|
storage_type=_STORAGE_BACKUP,
|
||||||
open_stream=open_stream,
|
open_stream=open_stream,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
base64md5hash=base64md5hash,
|
base64md5hash=base64md5hash,
|
||||||
@ -198,9 +146,19 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
size=size,
|
size=size,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except BackupAgentError as err:
|
except CloudApiNonRetryableError as err:
|
||||||
|
if err.code == "NC-SH-FH-03":
|
||||||
|
raise BackupAgentError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="backup_size_too_large",
|
||||||
|
translation_placeholders={
|
||||||
|
"size": str(round(size / (1024**3), 2))
|
||||||
|
},
|
||||||
|
) from err
|
||||||
|
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||||
|
except CloudError as err:
|
||||||
if tries == _RETRY_LIMIT:
|
if tries == _RETRY_LIMIT:
|
||||||
raise
|
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||||
tries += 1
|
tries += 1
|
||||||
retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX)
|
retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX)
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
|
@ -17,6 +17,11 @@
|
|||||||
"subscription_expiration": "Subscription expiration"
|
"subscription_expiration": "Subscription expiration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"backup_size_too_large": {
|
||||||
|
"message": "The backup size of {size}GB is too large to be uploaded to Home Assistant Cloud."
|
||||||
|
}
|
||||||
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"deprecated_gender": {
|
"deprecated_gender": {
|
||||||
"title": "The {deprecated_option} text-to-speech option is deprecated",
|
"title": "The {deprecated_option} text-to-speech option is deprecated",
|
||||||
|
@ -132,6 +132,7 @@ WALLETS = {
|
|||||||
"GYD": "GYD",
|
"GYD": "GYD",
|
||||||
"HKD": "HKD",
|
"HKD": "HKD",
|
||||||
"HNL": "HNL",
|
"HNL": "HNL",
|
||||||
|
"HNT": "HNT",
|
||||||
"HRK": "HRK",
|
"HRK": "HRK",
|
||||||
"HTG": "HTG",
|
"HTG": "HTG",
|
||||||
"HUF": "HUF",
|
"HUF": "HUF",
|
||||||
@ -410,6 +411,7 @@ RATES = {
|
|||||||
"GYEN": "GYEN",
|
"GYEN": "GYEN",
|
||||||
"HKD": "HKD",
|
"HKD": "HKD",
|
||||||
"HNL": "HNL",
|
"HNL": "HNL",
|
||||||
|
"HNT": "HNT",
|
||||||
"HRK": "HRK",
|
"HRK": "HRK",
|
||||||
"HTG": "HTG",
|
"HTG": "HTG",
|
||||||
"HUF": "HUF",
|
"HUF": "HUF",
|
||||||
|
@ -2,19 +2,19 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Coinbase API Key Details",
|
"title": "Coinbase API key details",
|
||||||
"description": "Please enter the details of your API key as provided by Coinbase.",
|
"description": "Please enter the details of your API key as provided by Coinbase.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
"api_token": "API Secret"
|
"api_token": "API secret"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"invalid_auth_key": "API credentials rejected by Coinbase due to an invalid API Key.",
|
"invalid_auth_key": "API credentials rejected by Coinbase due to an invalid API key.",
|
||||||
"invalid_auth_secret": "API credentials rejected by Coinbase due to an invalid API Secret.",
|
"invalid_auth_secret": "API credentials rejected by Coinbase due to an invalid API secret.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
@ -24,12 +24,12 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"description": "Adjust Coinbase Options",
|
"description": "Adjust Coinbase options",
|
||||||
"data": {
|
"data": {
|
||||||
"account_balance_currencies": "Wallet balances to report.",
|
"account_balance_currencies": "Wallet balances to report.",
|
||||||
"exchange_rate_currencies": "Exchange rates to report.",
|
"exchange_rate_currencies": "Exchange rates to report.",
|
||||||
"exchange_base": "Base currency for exchange rate sensors.",
|
"exchange_base": "Base currency for exchange rate sensors.",
|
||||||
"exchnage_rate_precision": "Number of decimal places for exchange rates."
|
"exchange_rate_precision": "Number of decimal places for exchange rates."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -43,13 +43,6 @@ def async_get_chat_log(
|
|||||||
else:
|
else:
|
||||||
history = ChatLog(hass, session.conversation_id)
|
history = ChatLog(hass, session.conversation_id)
|
||||||
|
|
||||||
@callback
|
|
||||||
def do_cleanup() -> None:
|
|
||||||
"""Handle cleanup."""
|
|
||||||
all_history.pop(session.conversation_id)
|
|
||||||
|
|
||||||
session.async_on_cleanup(do_cleanup)
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
history.async_add_user_content(UserContent(content=user_input.text))
|
history.async_add_user_content(UserContent(content=user_input.text))
|
||||||
|
|
||||||
@ -63,6 +56,15 @@ def async_get_chat_log(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if session.conversation_id not in all_history:
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def do_cleanup() -> None:
|
||||||
|
"""Handle cleanup."""
|
||||||
|
all_history.pop(session.conversation_id)
|
||||||
|
|
||||||
|
session.async_on_cleanup(do_cleanup)
|
||||||
|
|
||||||
all_history[session.conversation_id] = history
|
all_history[session.conversation_id] = history
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==2.2.0", "home-assistant-intents==2025.1.28"]
|
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.2.5"]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["eheimdigital"],
|
"loggers": ["eheimdigital"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["eheimdigital==1.0.5"],
|
"requirements": ["eheimdigital==1.0.6"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
|
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
|
||||||
]
|
]
|
||||||
|
@ -4,12 +4,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from electrickiwi_api import ElectricKiwiApi
|
from electrickiwi_api import ElectricKiwiApi
|
||||||
from electrickiwi_api.exceptions import ApiException
|
from electrickiwi_api.exceptions import ApiException, AuthException
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
from homeassistant.helpers import (
|
||||||
|
aiohttp_client,
|
||||||
|
config_entry_oauth2_flow,
|
||||||
|
entity_registry as er,
|
||||||
|
)
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
@ -44,7 +48,9 @@ async def async_setup_entry(
|
|||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
ek_api = ElectricKiwiApi(
|
ek_api = ElectricKiwiApi(
|
||||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
api.ConfigEntryElectricKiwiAuth(
|
||||||
|
aiohttp_client.async_get_clientsession(hass), session
|
||||||
|
)
|
||||||
)
|
)
|
||||||
hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, entry, ek_api)
|
hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, entry, ek_api)
|
||||||
account_coordinator = ElectricKiwiAccountDataCoordinator(hass, entry, ek_api)
|
account_coordinator = ElectricKiwiAccountDataCoordinator(hass, entry, ek_api)
|
||||||
@ -53,6 +59,8 @@ async def async_setup_entry(
|
|||||||
await ek_api.set_active_session()
|
await ek_api.set_active_session()
|
||||||
await hop_coordinator.async_config_entry_first_refresh()
|
await hop_coordinator.async_config_entry_first_refresh()
|
||||||
await account_coordinator.async_config_entry_first_refresh()
|
await account_coordinator.async_config_entry_first_refresh()
|
||||||
|
except AuthException as err:
|
||||||
|
raise ConfigEntryAuthFailed from err
|
||||||
except ApiException as err:
|
except ApiException as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
@ -70,3 +78,53 @@ async def async_unload_entry(
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(
|
||||||
|
hass: HomeAssistant, config_entry: ElectricKiwiConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Migrate old entry."""
|
||||||
|
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||||
|
implementation = (
|
||||||
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
|
hass, config_entry
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
session = config_entry_oauth2_flow.OAuth2Session(
|
||||||
|
hass, config_entry, implementation
|
||||||
|
)
|
||||||
|
|
||||||
|
ek_api = ElectricKiwiApi(
|
||||||
|
api.ConfigEntryElectricKiwiAuth(
|
||||||
|
aiohttp_client.async_get_clientsession(hass), session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await ek_api.set_active_session()
|
||||||
|
connection_details = await ek_api.get_connection_details()
|
||||||
|
except AuthException:
|
||||||
|
config_entry.async_start_reauth(hass)
|
||||||
|
return False
|
||||||
|
except ApiException:
|
||||||
|
return False
|
||||||
|
unique_id = str(ek_api.customer_number)
|
||||||
|
identifier = ek_api.electricity.identifier
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
config_entry, unique_id=unique_id, minor_version=2
|
||||||
|
)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
entity_entries = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, config_entry_id=config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
for entity in entity_entries:
|
||||||
|
assert entity.config_entry_id
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity.entity_id,
|
||||||
|
new_unique_id=entity.unique_id.replace(
|
||||||
|
f"{unique_id}_{connection_details.id}", f"{unique_id}_{identifier}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -2,17 +2,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from electrickiwi_api import AbstractAuth
|
from electrickiwi_api import AbstractAuth
|
||||||
|
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||||
|
|
||||||
from .const import API_BASE_URL
|
from .const import API_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
class AsyncConfigEntryAuth(AbstractAuth):
|
class ConfigEntryElectricKiwiAuth(AbstractAuth):
|
||||||
"""Provide Electric Kiwi authentication tied to an OAuth2 based config entry."""
|
"""Provide Electric Kiwi authentication tied to an OAuth2 based config entry."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -29,4 +28,21 @@ class AsyncConfigEntryAuth(AbstractAuth):
|
|||||||
"""Return a valid access token."""
|
"""Return a valid access token."""
|
||||||
await self._oauth_session.async_ensure_token_valid()
|
await self._oauth_session.async_ensure_token_valid()
|
||||||
|
|
||||||
return cast(str, self._oauth_session.token["access_token"])
|
return str(self._oauth_session.token["access_token"])
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlowElectricKiwiAuth(AbstractAuth):
|
||||||
|
"""Provide Electric Kiwi authentication tied to an OAuth2 based config flow."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
token: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize ConfigFlowFitbitApi."""
|
||||||
|
super().__init__(aiohttp_client.async_get_clientsession(hass), API_BASE_URL)
|
||||||
|
self._token = token
|
||||||
|
|
||||||
|
async def async_get_access_token(self) -> str:
|
||||||
|
"""Return the token for the Electric Kiwi API."""
|
||||||
|
return self._token
|
||||||
|
@ -6,9 +6,14 @@ from collections.abc import Mapping
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlowResult
|
from electrickiwi_api import ElectricKiwiApi
|
||||||
|
from electrickiwi_api.exceptions import ApiException
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from . import api
|
||||||
from .const import DOMAIN, SCOPE_VALUES
|
from .const import DOMAIN, SCOPE_VALUES
|
||||||
|
|
||||||
|
|
||||||
@ -17,6 +22,8 @@ class ElectricKiwiOauth2FlowHandler(
|
|||||||
):
|
):
|
||||||
"""Config flow to handle Electric Kiwi OAuth2 authentication."""
|
"""Config flow to handle Electric Kiwi OAuth2 authentication."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
DOMAIN = DOMAIN
|
DOMAIN = DOMAIN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -40,12 +47,30 @@ class ElectricKiwiOauth2FlowHandler(
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Dialog that informs the user that reauth is required."""
|
"""Dialog that informs the user that reauth is required."""
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(step_id="reauth_confirm")
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
description_placeholders={CONF_NAME: self._get_reauth_entry().title},
|
||||||
|
)
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||||
"""Create an entry for Electric Kiwi."""
|
"""Create an entry for Electric Kiwi."""
|
||||||
existing_entry = await self.async_set_unique_id(DOMAIN)
|
ek_api = ElectricKiwiApi(
|
||||||
if existing_entry:
|
api.ConfigFlowElectricKiwiAuth(self.hass, data["token"]["access_token"])
|
||||||
return self.async_update_reload_and_abort(existing_entry, data=data)
|
)
|
||||||
return await super().async_oauth_create_entry(data)
|
|
||||||
|
try:
|
||||||
|
session = await ek_api.get_active_session()
|
||||||
|
except ApiException:
|
||||||
|
return self.async_abort(reason="connection_error")
|
||||||
|
|
||||||
|
unique_id = str(session.data.customer_number)
|
||||||
|
await self.async_set_unique_id(unique_id)
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(), data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title=unique_id, data=data)
|
||||||
|
@ -8,4 +8,4 @@ OAUTH2_AUTHORIZE = "https://welcome.electrickiwi.co.nz/oauth/authorize"
|
|||||||
OAUTH2_TOKEN = "https://welcome.electrickiwi.co.nz/oauth/token"
|
OAUTH2_TOKEN = "https://welcome.electrickiwi.co.nz/oauth/token"
|
||||||
API_BASE_URL = "https://api.electrickiwi.co.nz"
|
API_BASE_URL = "https://api.electrickiwi.co.nz"
|
||||||
|
|
||||||
SCOPE_VALUES = "read_connection_detail read_billing_frequency read_account_running_balance read_consumption_summary read_consumption_averages read_hop_intervals_config read_hop_connection save_hop_connection read_session"
|
SCOPE_VALUES = "read_customer_details read_connection_detail read_connection read_billing_address get_bill_address read_billing_frequency read_billing_details read_billing_bills read_billing_bill read_billing_bill_id read_billing_bill_file read_account_running_balance read_customer_account_summary read_consumption_summary download_consumption_file read_consumption_averages get_consumption_averages read_hop_intervals_config read_hop_intervals read_hop_connection read_hop_specific_connection save_hop_connection save_hop_specific_connection read_outage_contact get_outage_contact_info_for_icp read_session read_session_data_login"
|
||||||
|
@ -10,7 +10,7 @@ import logging
|
|||||||
|
|
||||||
from electrickiwi_api import ElectricKiwiApi
|
from electrickiwi_api import ElectricKiwiApi
|
||||||
from electrickiwi_api.exceptions import ApiException, AuthException
|
from electrickiwi_api.exceptions import ApiException, AuthException
|
||||||
from electrickiwi_api.model import AccountBalance, Hop, HopIntervals
|
from electrickiwi_api.model import AccountSummary, Hop, HopIntervals
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -34,7 +34,7 @@ class ElectricKiwiRuntimeData:
|
|||||||
type ElectricKiwiConfigEntry = ConfigEntry[ElectricKiwiRuntimeData]
|
type ElectricKiwiConfigEntry = ConfigEntry[ElectricKiwiRuntimeData]
|
||||||
|
|
||||||
|
|
||||||
class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountBalance]):
|
class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountSummary]):
|
||||||
"""ElectricKiwi Account Data object."""
|
"""ElectricKiwi Account Data object."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -51,13 +51,13 @@ class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountBalance]):
|
|||||||
name="Electric Kiwi Account Data",
|
name="Electric Kiwi Account Data",
|
||||||
update_interval=ACCOUNT_SCAN_INTERVAL,
|
update_interval=ACCOUNT_SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
self._ek_api = ek_api
|
self.ek_api = ek_api
|
||||||
|
|
||||||
async def _async_update_data(self) -> AccountBalance:
|
async def _async_update_data(self) -> AccountSummary:
|
||||||
"""Fetch data from Account balance API endpoint."""
|
"""Fetch data from Account balance API endpoint."""
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(60):
|
async with asyncio.timeout(60):
|
||||||
return await self._ek_api.get_account_balance()
|
return await self.ek_api.get_account_summary()
|
||||||
except AuthException as auth_err:
|
except AuthException as auth_err:
|
||||||
raise ConfigEntryAuthFailed from auth_err
|
raise ConfigEntryAuthFailed from auth_err
|
||||||
except ApiException as api_err:
|
except ApiException as api_err:
|
||||||
@ -85,7 +85,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
# Polling interval. Will only be polled if there are subscribers.
|
# Polling interval. Will only be polled if there are subscribers.
|
||||||
update_interval=HOP_SCAN_INTERVAL,
|
update_interval=HOP_SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
self._ek_api = ek_api
|
self.ek_api = ek_api
|
||||||
self.hop_intervals: HopIntervals | None = None
|
self.hop_intervals: HopIntervals | None = None
|
||||||
|
|
||||||
def get_hop_options(self) -> dict[str, int]:
|
def get_hop_options(self) -> dict[str, int]:
|
||||||
@ -100,7 +100,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
async def async_update_hop(self, hop_interval: int) -> Hop:
|
async def async_update_hop(self, hop_interval: int) -> Hop:
|
||||||
"""Update selected hop and data."""
|
"""Update selected hop and data."""
|
||||||
try:
|
try:
|
||||||
self.async_set_updated_data(await self._ek_api.post_hop(hop_interval))
|
self.async_set_updated_data(await self.ek_api.post_hop(hop_interval))
|
||||||
except AuthException as auth_err:
|
except AuthException as auth_err:
|
||||||
raise ConfigEntryAuthFailed from auth_err
|
raise ConfigEntryAuthFailed from auth_err
|
||||||
except ApiException as api_err:
|
except ApiException as api_err:
|
||||||
@ -118,7 +118,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
try:
|
try:
|
||||||
async with asyncio.timeout(60):
|
async with asyncio.timeout(60):
|
||||||
if self.hop_intervals is None:
|
if self.hop_intervals is None:
|
||||||
hop_intervals: HopIntervals = await self._ek_api.get_hop_intervals()
|
hop_intervals: HopIntervals = await self.ek_api.get_hop_intervals()
|
||||||
hop_intervals.intervals = OrderedDict(
|
hop_intervals.intervals = OrderedDict(
|
||||||
filter(
|
filter(
|
||||||
lambda pair: pair[1].active == 1,
|
lambda pair: pair[1].active == 1,
|
||||||
@ -127,7 +127,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.hop_intervals = hop_intervals
|
self.hop_intervals = hop_intervals
|
||||||
return await self._ek_api.get_hop()
|
return await self.ek_api.get_hop()
|
||||||
except AuthException as auth_err:
|
except AuthException as auth_err:
|
||||||
raise ConfigEntryAuthFailed from auth_err
|
raise ConfigEntryAuthFailed from auth_err
|
||||||
except ApiException as api_err:
|
except ApiException as api_err:
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/electric_kiwi",
|
"documentation": "https://www.home-assistant.io/integrations/electric_kiwi",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["electrickiwi-api==0.8.5"]
|
"requirements": ["electrickiwi-api==0.9.14"]
|
||||||
}
|
}
|
||||||
|
@ -53,8 +53,8 @@ class ElectricKiwiSelectHOPEntity(
|
|||||||
"""Initialise the HOP selection entity."""
|
"""Initialise the HOP selection entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
f"{coordinator.ek_api.customer_number}"
|
||||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self.values_dict = coordinator.get_hop_options()
|
self.values_dict = coordinator.get_hop_options()
|
||||||
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from electrickiwi_api.model import AccountBalance, Hop
|
from electrickiwi_api.model import AccountSummary, Hop
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -39,7 +39,15 @@ ATTR_HOP_PERCENTAGE = "hop_percentage"
|
|||||||
class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription):
|
class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describes Electric Kiwi sensor entity."""
|
"""Describes Electric Kiwi sensor entity."""
|
||||||
|
|
||||||
value_func: Callable[[AccountBalance], float | datetime]
|
value_func: Callable[[AccountSummary], float | datetime]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_hop_percentage(account_balance: AccountSummary) -> float:
|
||||||
|
"""Return the hop percentage from account summary."""
|
||||||
|
if power := account_balance.services.get("power"):
|
||||||
|
if connection := power.connections[0]:
|
||||||
|
return float(connection.hop_percentage)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
||||||
@ -72,9 +80,7 @@ ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = (
|
|||||||
translation_key="hop_power_savings",
|
translation_key="hop_power_savings",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_func=lambda account_balance: float(
|
value_func=_get_hop_percentage,
|
||||||
account_balance.connections[0].hop_percentage
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -165,8 +171,8 @@ class ElectricKiwiAccountEntity(
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
f"{coordinator.ek_api.customer_number}"
|
||||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
@ -194,8 +200,8 @@ class ElectricKiwiHOPEntity(
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator._ek_api.customer_number}" # noqa: SLF001
|
f"{coordinator.ek_api.customer_number}"
|
||||||
f"_{coordinator._ek_api.connection_id}_{description.key}" # noqa: SLF001
|
f"_{coordinator.ek_api.electricity.identifier}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@
|
|||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
|
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||||
|
"connection_error": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
},
|
},
|
||||||
"create_entry": {
|
"create_entry": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
@ -3,29 +3,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
|
|
||||||
from pyfireservicerota import (
|
|
||||||
ExpiredTokenError,
|
|
||||||
FireServiceRota,
|
|
||||||
FireServiceRotaIncidents,
|
|
||||||
InvalidAuthError,
|
|
||||||
InvalidTokenError,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME, Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN, WSS_BWRURL
|
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||||
|
from .coordinator import FireServiceRotaClient, FireServiceUpdateCoordinator
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
@ -40,17 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
if client.token_refresh_failure:
|
if client.token_refresh_failure:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_update_data():
|
coordinator = FireServiceUpdateCoordinator(hass, client, entry)
|
||||||
return await client.async_update()
|
|
||||||
|
|
||||||
coordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
config_entry=entry,
|
|
||||||
name="duty binary sensor",
|
|
||||||
update_method=async_update_data,
|
|
||||||
update_interval=MIN_TIME_BETWEEN_UPDATES,
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
@ -68,171 +45,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
"""Unload FireServiceRota config entry."""
|
"""Unload FireServiceRota config entry."""
|
||||||
|
|
||||||
await hass.async_add_executor_job(
|
await hass.async_add_executor_job(
|
||||||
hass.data[DOMAIN][entry.entry_id].websocket.stop_listener
|
hass.data[DOMAIN][entry.entry_id][DATA_CLIENT].websocket.stop_listener
|
||||||
)
|
)
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
del hass.data[DOMAIN][entry.entry_id]
|
del hass.data[DOMAIN][entry.entry_id]
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
class FireServiceRotaOauth:
|
|
||||||
"""Handle authentication tokens."""
|
|
||||||
|
|
||||||
def __init__(self, hass, entry, fsr):
|
|
||||||
"""Initialize the oauth object."""
|
|
||||||
self._hass = hass
|
|
||||||
self._entry = entry
|
|
||||||
|
|
||||||
self._url = entry.data[CONF_URL]
|
|
||||||
self._username = entry.data[CONF_USERNAME]
|
|
||||||
self._fsr = fsr
|
|
||||||
|
|
||||||
async def async_refresh_tokens(self) -> bool:
|
|
||||||
"""Refresh tokens and update config entry."""
|
|
||||||
_LOGGER.debug("Refreshing authentication tokens after expiration")
|
|
||||||
|
|
||||||
try:
|
|
||||||
token_info = await self._hass.async_add_executor_job(
|
|
||||||
self._fsr.refresh_tokens
|
|
||||||
)
|
|
||||||
|
|
||||||
except (InvalidAuthError, InvalidTokenError) as err:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
"Error refreshing tokens, triggered reauth workflow"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
_LOGGER.debug("Saving new tokens in config entry")
|
|
||||||
self._hass.config_entries.async_update_entry(
|
|
||||||
self._entry,
|
|
||||||
data={
|
|
||||||
"auth_implementation": DOMAIN,
|
|
||||||
CONF_URL: self._url,
|
|
||||||
CONF_USERNAME: self._username,
|
|
||||||
CONF_TOKEN: token_info,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class FireServiceRotaWebSocket:
|
|
||||||
"""Define a FireServiceRota websocket manager object."""
|
|
||||||
|
|
||||||
def __init__(self, hass, entry):
|
|
||||||
"""Initialize the websocket object."""
|
|
||||||
self._hass = hass
|
|
||||||
self._entry = entry
|
|
||||||
|
|
||||||
self._fsr_incidents = FireServiceRotaIncidents(on_incident=self._on_incident)
|
|
||||||
self.incident_data = None
|
|
||||||
|
|
||||||
def _construct_url(self) -> str:
|
|
||||||
"""Return URL with latest access token."""
|
|
||||||
return WSS_BWRURL.format(
|
|
||||||
self._entry.data[CONF_URL], self._entry.data[CONF_TOKEN]["access_token"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_incident(self, data) -> None:
|
|
||||||
"""Received new incident, update data."""
|
|
||||||
_LOGGER.debug("Received new incident via websocket: %s", data)
|
|
||||||
self.incident_data = data
|
|
||||||
dispatcher_send(self._hass, f"{DOMAIN}_{self._entry.entry_id}_update")
|
|
||||||
|
|
||||||
def start_listener(self) -> None:
|
|
||||||
"""Start the websocket listener."""
|
|
||||||
_LOGGER.debug("Starting incidents listener")
|
|
||||||
self._fsr_incidents.start(self._construct_url())
|
|
||||||
|
|
||||||
def stop_listener(self) -> None:
|
|
||||||
"""Stop the websocket listener."""
|
|
||||||
_LOGGER.debug("Stopping incidents listener")
|
|
||||||
self._fsr_incidents.stop()
|
|
||||||
|
|
||||||
|
|
||||||
class FireServiceRotaClient:
|
|
||||||
"""Getting the latest data from fireservicerota."""
|
|
||||||
|
|
||||||
def __init__(self, hass, entry):
|
|
||||||
"""Initialize the data object."""
|
|
||||||
self._hass = hass
|
|
||||||
self._entry = entry
|
|
||||||
|
|
||||||
self._url = entry.data[CONF_URL]
|
|
||||||
self._tokens = entry.data[CONF_TOKEN]
|
|
||||||
|
|
||||||
self.entry_id = entry.entry_id
|
|
||||||
self.unique_id = entry.unique_id
|
|
||||||
|
|
||||||
self.token_refresh_failure = False
|
|
||||||
self.incident_id = None
|
|
||||||
self.on_duty = False
|
|
||||||
|
|
||||||
self.fsr = FireServiceRota(base_url=self._url, token_info=self._tokens)
|
|
||||||
|
|
||||||
self.oauth = FireServiceRotaOauth(
|
|
||||||
self._hass,
|
|
||||||
self._entry,
|
|
||||||
self.fsr,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.websocket = FireServiceRotaWebSocket(self._hass, self._entry)
|
|
||||||
|
|
||||||
async def setup(self) -> None:
|
|
||||||
"""Set up the data client."""
|
|
||||||
await self._hass.async_add_executor_job(self.websocket.start_listener)
|
|
||||||
|
|
||||||
async def update_call(self, func, *args):
|
|
||||||
"""Perform update call and return data."""
|
|
||||||
if self.token_refresh_failure:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await self._hass.async_add_executor_job(func, *args)
|
|
||||||
except (ExpiredTokenError, InvalidTokenError):
|
|
||||||
await self._hass.async_add_executor_job(self.websocket.stop_listener)
|
|
||||||
self.token_refresh_failure = True
|
|
||||||
|
|
||||||
if await self.oauth.async_refresh_tokens():
|
|
||||||
self.token_refresh_failure = False
|
|
||||||
await self._hass.async_add_executor_job(self.websocket.start_listener)
|
|
||||||
|
|
||||||
return await self._hass.async_add_executor_job(func, *args)
|
|
||||||
|
|
||||||
async def async_update(self) -> dict | None:
|
|
||||||
"""Get the latest availability data."""
|
|
||||||
data = await self.update_call(
|
|
||||||
self.fsr.get_availability, str(self._hass.config.time_zone)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.on_duty = bool(data.get("available"))
|
|
||||||
|
|
||||||
_LOGGER.debug("Updated availability data: %s", data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def async_response_update(self) -> dict | None:
|
|
||||||
"""Get the latest incident response data."""
|
|
||||||
|
|
||||||
if not self.incident_id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
_LOGGER.debug("Updating response data for incident id %s", self.incident_id)
|
|
||||||
|
|
||||||
return await self.update_call(self.fsr.get_incident_response, self.incident_id)
|
|
||||||
|
|
||||||
async def async_set_response(self, value) -> None:
|
|
||||||
"""Set incident response status."""
|
|
||||||
|
|
||||||
if not self.incident_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Setting incident response for incident id '%s' to state '%s'",
|
|
||||||
self.incident_id,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.update_call(self.fsr.set_incident_response, self.incident_id, value)
|
|
||||||
|
@ -8,13 +8,10 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import FireServiceRotaClient
|
|
||||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN
|
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN
|
||||||
|
from .coordinator import FireServiceRotaClient, FireServiceUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -26,14 +23,16 @@ async def async_setup_entry(
|
|||||||
DATA_CLIENT
|
DATA_CLIENT
|
||||||
]
|
]
|
||||||
|
|
||||||
coordinator: DataUpdateCoordinator = hass.data[FIRESERVICEROTA_DOMAIN][
|
coordinator: FireServiceUpdateCoordinator = hass.data[FIRESERVICEROTA_DOMAIN][
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
][DATA_COORDINATOR]
|
][DATA_COORDINATOR]
|
||||||
|
|
||||||
async_add_entities([ResponseBinarySensor(coordinator, client, entry)])
|
async_add_entities([ResponseBinarySensor(coordinator, client, entry)])
|
||||||
|
|
||||||
|
|
||||||
class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity):
|
class ResponseBinarySensor(
|
||||||
|
CoordinatorEntity[FireServiceUpdateCoordinator], BinarySensorEntity
|
||||||
|
):
|
||||||
"""Representation of an FireServiceRota sensor."""
|
"""Representation of an FireServiceRota sensor."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
@ -41,7 +40,7 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: FireServiceUpdateCoordinator,
|
||||||
client: FireServiceRotaClient,
|
client: FireServiceRotaClient,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
213
homeassistant/components/fireservicerota/coordinator.py
Normal file
213
homeassistant/components/fireservicerota/coordinator.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
"""The FireServiceRota integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyfireservicerota import (
|
||||||
|
ExpiredTokenError,
|
||||||
|
FireServiceRota,
|
||||||
|
FireServiceRotaIncidents,
|
||||||
|
InvalidAuthError,
|
||||||
|
InvalidTokenError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import DOMAIN, WSS_BWRURL
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
|
class FireServiceUpdateCoordinator(DataUpdateCoordinator[dict | None]):
|
||||||
|
"""Data update coordinator for FireServiceRota."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, client: FireServiceRotaClient, entry: ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the FireServiceRota DataUpdateCoordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="duty binary sensor",
|
||||||
|
config_entry=entry,
|
||||||
|
update_interval=MIN_TIME_BETWEEN_UPDATES,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict | None:
|
||||||
|
"""Get the latest availability data."""
|
||||||
|
return await self.client.async_update()
|
||||||
|
|
||||||
|
|
||||||
|
class FireServiceRotaOauth:
|
||||||
|
"""Handle authentication tokens."""
|
||||||
|
|
||||||
|
def __init__(self, hass, entry, fsr):
|
||||||
|
"""Initialize the oauth object."""
|
||||||
|
self._hass = hass
|
||||||
|
self._entry = entry
|
||||||
|
|
||||||
|
self._url = entry.data[CONF_URL]
|
||||||
|
self._username = entry.data[CONF_USERNAME]
|
||||||
|
self._fsr = fsr
|
||||||
|
|
||||||
|
async def async_refresh_tokens(self) -> bool:
|
||||||
|
"""Refresh tokens and update config entry."""
|
||||||
|
_LOGGER.debug("Refreshing authentication tokens after expiration")
|
||||||
|
|
||||||
|
try:
|
||||||
|
token_info = await self._hass.async_add_executor_job(
|
||||||
|
self._fsr.refresh_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
except (InvalidAuthError, InvalidTokenError) as err:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
"Error refreshing tokens, triggered reauth workflow"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
_LOGGER.debug("Saving new tokens in config entry")
|
||||||
|
self._hass.config_entries.async_update_entry(
|
||||||
|
self._entry,
|
||||||
|
data={
|
||||||
|
"auth_implementation": DOMAIN,
|
||||||
|
CONF_URL: self._url,
|
||||||
|
CONF_USERNAME: self._username,
|
||||||
|
CONF_TOKEN: token_info,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FireServiceRotaWebSocket:
|
||||||
|
"""Define a FireServiceRota websocket manager object."""
|
||||||
|
|
||||||
|
def __init__(self, hass, entry):
|
||||||
|
"""Initialize the websocket object."""
|
||||||
|
self._hass = hass
|
||||||
|
self._entry = entry
|
||||||
|
|
||||||
|
self._fsr_incidents = FireServiceRotaIncidents(on_incident=self._on_incident)
|
||||||
|
self.incident_data = None
|
||||||
|
|
||||||
|
def _construct_url(self) -> str:
|
||||||
|
"""Return URL with latest access token."""
|
||||||
|
return WSS_BWRURL.format(
|
||||||
|
self._entry.data[CONF_URL], self._entry.data[CONF_TOKEN]["access_token"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_incident(self, data) -> None:
|
||||||
|
"""Received new incident, update data."""
|
||||||
|
_LOGGER.debug("Received new incident via websocket: %s", data)
|
||||||
|
self.incident_data = data
|
||||||
|
dispatcher_send(self._hass, f"{DOMAIN}_{self._entry.entry_id}_update")
|
||||||
|
|
||||||
|
def start_listener(self) -> None:
|
||||||
|
"""Start the websocket listener."""
|
||||||
|
_LOGGER.debug("Starting incidents listener")
|
||||||
|
self._fsr_incidents.start(self._construct_url())
|
||||||
|
|
||||||
|
def stop_listener(self) -> None:
|
||||||
|
"""Stop the websocket listener."""
|
||||||
|
_LOGGER.debug("Stopping incidents listener")
|
||||||
|
self._fsr_incidents.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class FireServiceRotaClient:
|
||||||
|
"""Getting the latest data from fireservicerota."""
|
||||||
|
|
||||||
|
def __init__(self, hass, entry):
|
||||||
|
"""Initialize the data object."""
|
||||||
|
self._hass = hass
|
||||||
|
self._entry = entry
|
||||||
|
|
||||||
|
self._url = entry.data[CONF_URL]
|
||||||
|
self._tokens = entry.data[CONF_TOKEN]
|
||||||
|
|
||||||
|
self.entry_id = entry.entry_id
|
||||||
|
self.unique_id = entry.unique_id
|
||||||
|
|
||||||
|
self.token_refresh_failure = False
|
||||||
|
self.incident_id = None
|
||||||
|
self.on_duty = False
|
||||||
|
|
||||||
|
self.fsr = FireServiceRota(base_url=self._url, token_info=self._tokens)
|
||||||
|
|
||||||
|
self.oauth = FireServiceRotaOauth(
|
||||||
|
self._hass,
|
||||||
|
self._entry,
|
||||||
|
self.fsr,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.websocket = FireServiceRotaWebSocket(self._hass, self._entry)
|
||||||
|
|
||||||
|
async def setup(self) -> None:
|
||||||
|
"""Set up the data client."""
|
||||||
|
await self._hass.async_add_executor_job(self.websocket.start_listener)
|
||||||
|
|
||||||
|
async def update_call(self, func, *args):
|
||||||
|
"""Perform update call and return data."""
|
||||||
|
if self.token_refresh_failure:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await self._hass.async_add_executor_job(func, *args)
|
||||||
|
except (ExpiredTokenError, InvalidTokenError):
|
||||||
|
await self._hass.async_add_executor_job(self.websocket.stop_listener)
|
||||||
|
self.token_refresh_failure = True
|
||||||
|
|
||||||
|
if await self.oauth.async_refresh_tokens():
|
||||||
|
self.token_refresh_failure = False
|
||||||
|
await self._hass.async_add_executor_job(self.websocket.start_listener)
|
||||||
|
|
||||||
|
return await self._hass.async_add_executor_job(func, *args)
|
||||||
|
|
||||||
|
async def async_update(self) -> dict | None:
|
||||||
|
"""Get the latest availability data."""
|
||||||
|
data = await self.update_call(
|
||||||
|
self.fsr.get_availability, str(self._hass.config.time_zone)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.on_duty = bool(data.get("available"))
|
||||||
|
|
||||||
|
_LOGGER.debug("Updated availability data: %s", data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def async_response_update(self) -> dict | None:
|
||||||
|
"""Get the latest incident response data."""
|
||||||
|
|
||||||
|
if not self.incident_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
_LOGGER.debug("Updating response data for incident id %s", self.incident_id)
|
||||||
|
|
||||||
|
return await self.update_call(self.fsr.get_incident_response, self.incident_id)
|
||||||
|
|
||||||
|
async def async_set_response(self, value) -> None:
|
||||||
|
"""Set incident response status."""
|
||||||
|
|
||||||
|
if not self.incident_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Setting incident response for incident id '%s' to state '%s'",
|
||||||
|
self.incident_id,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.update_call(self.fsr.set_incident_response, self.incident_id, value)
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
|
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyfireservicerota"],
|
"loggers": ["pyfireservicerota"],
|
||||||
"requirements": ["pyfireservicerota==0.0.43"]
|
"requirements": ["pyfireservicerota==0.0.46"]
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
from .const import DOMAIN, FitbitScope
|
from .const import FitbitScope
|
||||||
from .coordinator import FitbitData, FitbitDeviceCoordinator
|
from .coordinator import FitbitData, FitbitDeviceCoordinator
|
||||||
from .exceptions import FitbitApiException, FitbitAuthException
|
from .exceptions import FitbitApiException, FitbitAuthException
|
||||||
from .model import config_from_entry_data
|
from .model import config_from_entry_data
|
||||||
@ -15,10 +15,11 @@ from .model import config_from_entry_data
|
|||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
type FitbitConfigEntry = ConfigEntry[FitbitData]
|
||||||
"""Set up fitbit from a config entry."""
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: FitbitConfigEntry) -> bool:
|
||||||
|
"""Set up fitbit from a config entry."""
|
||||||
implementation = (
|
implementation = (
|
||||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
hass, entry
|
hass, entry
|
||||||
@ -41,18 +42,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
coordinator = FitbitDeviceCoordinator(hass, fitbit_api)
|
coordinator = FitbitDeviceCoordinator(hass, fitbit_api)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = FitbitData(
|
entry.runtime_data = FitbitData(api=fitbit_api, device_coordinator=coordinator)
|
||||||
api=fitbit_api, device_coordinator=coordinator
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: FitbitConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
@ -29,9 +28,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.icon import icon_for_battery_level
|
from homeassistant.helpers.icon import icon_for_battery_level
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import FitbitConfigEntry
|
||||||
from .api import FitbitApi
|
from .api import FitbitApi
|
||||||
from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem
|
from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem
|
||||||
from .coordinator import FitbitData, FitbitDeviceCoordinator
|
from .coordinator import FitbitDeviceCoordinator
|
||||||
from .exceptions import FitbitApiException, FitbitAuthException
|
from .exceptions import FitbitApiException, FitbitAuthException
|
||||||
from .model import FitbitDevice, config_from_entry_data
|
from .model import FitbitDevice, config_from_entry_data
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ class FitbitSensorEntityDescription(SensorEntityDescription):
|
|||||||
|
|
||||||
|
|
||||||
def _build_device_info(
|
def _build_device_info(
|
||||||
config_entry: ConfigEntry, entity_description: FitbitSensorEntityDescription
|
config_entry: FitbitConfigEntry, entity_description: FitbitSensorEntityDescription
|
||||||
) -> DeviceInfo:
|
) -> DeviceInfo:
|
||||||
"""Build device info for sensor entities info across devices."""
|
"""Build device info for sensor entities info across devices."""
|
||||||
unique_id = cast(str, config_entry.unique_id)
|
unique_id = cast(str, config_entry.unique_id)
|
||||||
@ -524,12 +524,12 @@ FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: FitbitConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Fitbit sensor platform."""
|
"""Set up the Fitbit sensor platform."""
|
||||||
|
|
||||||
data: FitbitData = hass.data[DOMAIN][entry.entry_id]
|
data = entry.runtime_data
|
||||||
api = data.api
|
api = data.api
|
||||||
|
|
||||||
# These are run serially to reuse the cached user profile, not gathered
|
# These are run serially to reuse the cached user profile, not gathered
|
||||||
@ -601,7 +601,7 @@ class FitbitSensor(SensorEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config_entry: ConfigEntry,
|
config_entry: FitbitConfigEntry,
|
||||||
api: FitbitApi,
|
api: FitbitApi,
|
||||||
user_profile_id: str,
|
user_profile_id: str,
|
||||||
description: FitbitSensorEntityDescription,
|
description: FitbitSensorEntityDescription,
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from libpyfoscam import FoscamCamera
|
from libpyfoscam import FoscamCamera
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@ -14,13 +13,13 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||||
|
|
||||||
from .config_flow import DEFAULT_RTSP_PORT
|
from .config_flow import DEFAULT_RTSP_PORT
|
||||||
from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
|
from .const import CONF_RTSP_PORT, LOGGER
|
||||||
from .coordinator import FoscamCoordinator
|
from .coordinator import FoscamConfigEntry, FoscamCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.CAMERA, Platform.SWITCH]
|
PLATFORMS = [Platform.CAMERA, Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bool:
|
||||||
"""Set up foscam from a config entry."""
|
"""Set up foscam from a config entry."""
|
||||||
|
|
||||||
session = FoscamCamera(
|
session = FoscamCamera(
|
||||||
@ -30,11 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
verbose=False,
|
verbose=False,
|
||||||
)
|
)
|
||||||
coordinator = FoscamCoordinator(hass, session)
|
coordinator = FoscamCoordinator(hass, entry, session)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
# Migrate to correct unique IDs for switches
|
# Migrate to correct unique IDs for switches
|
||||||
await async_migrate_entities(hass, entry)
|
await async_migrate_entities(hass, entry)
|
||||||
@ -44,20 +43,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return 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.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ)
|
|
||||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_migrate_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bool:
|
||||||
"""Migrate old entry."""
|
"""Migrate old entry."""
|
||||||
LOGGER.debug("Migrating from version %s", entry.version)
|
LOGGER.debug("Migrating from version %s", entry.version)
|
||||||
|
|
||||||
@ -97,7 +88,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entities(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def async_migrate_entities(hass: HomeAssistant, entry: FoscamConfigEntry) -> None:
|
||||||
"""Migrate old entry."""
|
"""Migrate old entry."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -7,21 +7,13 @@ import asyncio
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
|
||||||
CONF_RTSP_PORT,
|
from .coordinator import FoscamConfigEntry, FoscamCoordinator
|
||||||
CONF_STREAM,
|
|
||||||
DOMAIN,
|
|
||||||
LOGGER,
|
|
||||||
SERVICE_PTZ,
|
|
||||||
SERVICE_PTZ_PRESET,
|
|
||||||
)
|
|
||||||
from .coordinator import FoscamCoordinator
|
|
||||||
from .entity import FoscamEntity
|
from .entity import FoscamEntity
|
||||||
|
|
||||||
DIR_UP = "up"
|
DIR_UP = "up"
|
||||||
@ -56,7 +48,7 @@ PTZ_GOTO_PRESET_COMMAND = "ptz_goto_preset"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: FoscamConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a Foscam IP camera from a config entry."""
|
"""Add a Foscam IP camera from a config entry."""
|
||||||
@ -89,7 +81,7 @@ async def async_setup_entry(
|
|||||||
"async_perform_ptz_preset",
|
"async_perform_ptz_preset",
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator: FoscamCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
async_add_entities([HassFoscamCamera(coordinator, config_entry)])
|
async_add_entities([HassFoscamCamera(coordinator, config_entry)])
|
||||||
|
|
||||||
@ -103,7 +95,7 @@ class HassFoscamCamera(FoscamEntity, Camera):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: FoscamCoordinator,
|
coordinator: FoscamCoordinator,
|
||||||
config_entry: ConfigEntry,
|
config_entry: FoscamConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a Foscam camera."""
|
"""Initialize a Foscam camera."""
|
||||||
super().__init__(coordinator, config_entry.entry_id)
|
super().__init__(coordinator, config_entry.entry_id)
|
||||||
|
@ -6,11 +6,14 @@ from typing import Any
|
|||||||
|
|
||||||
from libpyfoscam import FoscamCamera
|
from libpyfoscam import FoscamCamera
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
type FoscamConfigEntry = ConfigEntry[FoscamCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
"""Foscam coordinator."""
|
"""Foscam coordinator."""
|
||||||
@ -18,12 +21,14 @@ class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
entry: FoscamConfigEntry,
|
||||||
session: FoscamCamera,
|
session: FoscamCamera,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize my coordinator."""
|
"""Initialize my coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=30),
|
update_interval=timedelta(seconds=30),
|
||||||
)
|
)
|
||||||
|
@ -5,24 +5,23 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FoscamCoordinator
|
from .const import LOGGER
|
||||||
from .const import DOMAIN, LOGGER
|
from .coordinator import FoscamConfigEntry, FoscamCoordinator
|
||||||
from .entity import FoscamEntity
|
from .entity import FoscamEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: FoscamConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up foscam switch from a config entry."""
|
"""Set up foscam switch from a config entry."""
|
||||||
|
|
||||||
coordinator: FoscamCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ class FoscamSleepSwitch(FoscamEntity, SwitchEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: FoscamCoordinator,
|
coordinator: FoscamCoordinator,
|
||||||
config_entry: ConfigEntry,
|
config_entry: FoscamConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a Foscam Sleep Switch."""
|
"""Initialize a Foscam Sleep Switch."""
|
||||||
super().__init__(coordinator, config_entry.entry_id)
|
super().__init__(coordinator, config_entry.entry_id)
|
||||||
|
@ -2,17 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.const import Platform
|
||||||
from homeassistant.const import CONF_API_KEY, Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PLATFORMS: Final[list[Platform]] = [
|
PLATFORMS: Final[list[Platform]] = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
@ -26,32 +21,27 @@ PLATFORMS: Final[list[Platform]] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: FreedomproConfigEntry) -> bool:
|
||||||
"""Set up Freedompro from a config entry."""
|
"""Set up Freedompro from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
coordinator = FreedomproDataUpdateCoordinator(hass, entry)
|
||||||
api_key = entry.data[CONF_API_KEY]
|
|
||||||
|
|
||||||
coordinator = FreedomproDataUpdateCoordinator(hass, api_key)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: FreedomproConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
async def update_listener(
|
||||||
|
hass: HomeAssistant, config_entry: FreedomproConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Update listener."""
|
"""Update listener."""
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
@ -6,14 +6,13 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
DEVICE_CLASS_MAP = {
|
DEVICE_CLASS_MAP = {
|
||||||
"smokeSensor": BinarySensorDeviceClass.SMOKE,
|
"smokeSensor": BinarySensorDeviceClass.SMOKE,
|
||||||
@ -33,10 +32,12 @@ SUPPORTED_SENSORS = {"smokeSensor", "occupancySensor", "motionSensor", "contactS
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro binary_sensor."""
|
"""Set up Freedompro binary_sensor."""
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(device, coordinator)
|
Device(device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -15,7 +15,6 @@ from homeassistant.components.climate import (
|
|||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -24,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -44,11 +43,13 @@ SUPPORTED_HVAC_MODES = [
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro climate."""
|
"""Set up Freedompro climate."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(
|
Device(
|
||||||
aiohttp_client.async_get_clientsession(hass), api_key, device, coordinator
|
aiohttp_client.async_get_clientsession(hass), api_key, device, coordinator
|
||||||
|
@ -8,6 +8,9 @@ from typing import Any
|
|||||||
|
|
||||||
from pyfreedompro import get_list, get_states
|
from pyfreedompro import get_list, get_states
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@ -15,18 +18,27 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type FreedomproConfigEntry = ConfigEntry[FreedomproDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class FreedomproDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
class FreedomproDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||||
"""Class to manage fetching Freedompro data API."""
|
"""Class to manage fetching Freedompro data API."""
|
||||||
|
|
||||||
def __init__(self, hass, api_key):
|
def __init__(self, hass: HomeAssistant, entry: FreedomproConfigEntry) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
|
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._api_key = api_key
|
self._api_key = entry.data[CONF_API_KEY]
|
||||||
self._devices: list[dict[str, Any]] | None = None
|
self._devices: list[dict[str, Any]] | None = None
|
||||||
|
|
||||||
update_interval = timedelta(minutes=1)
|
update_interval = timedelta(minutes=1)
|
||||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=update_interval,
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_update_data(self):
|
async def _async_update_data(self):
|
||||||
if self._devices is None:
|
if self._devices is None:
|
||||||
|
@ -11,7 +11,6 @@ from homeassistant.components.cover import (
|
|||||||
CoverEntity,
|
CoverEntity,
|
||||||
CoverEntityFeature,
|
CoverEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -20,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
DEVICE_CLASS_MAP = {
|
DEVICE_CLASS_MAP = {
|
||||||
"windowCovering": CoverDeviceClass.BLIND,
|
"windowCovering": CoverDeviceClass.BLIND,
|
||||||
@ -34,11 +33,13 @@ SUPPORTED_SENSORS = {"windowCovering", "gate", "garageDoor", "door", "window"}
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro cover."""
|
"""Set up Freedompro cover."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(hass, api_key, device, coordinator)
|
Device(hass, api_key, device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -8,7 +8,6 @@ from typing import Any
|
|||||||
from pyfreedompro import put_state
|
from pyfreedompro import put_state
|
||||||
|
|
||||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -17,15 +16,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro fan."""
|
"""Set up Freedompro fan."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
FreedomproFan(hass, api_key, device, coordinator)
|
FreedomproFan(hass, api_key, device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -13,7 +13,6 @@ from homeassistant.components.light import (
|
|||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -22,15 +21,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro light."""
|
"""Set up Freedompro light."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(hass, api_key, device, coordinator)
|
Device(hass, api_key, device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -6,7 +6,6 @@ from typing import Any
|
|||||||
from pyfreedompro import put_state
|
from pyfreedompro import put_state
|
||||||
|
|
||||||
from homeassistant.components.lock import LockEntity
|
from homeassistant.components.lock import LockEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -15,15 +14,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro lock."""
|
"""Set up Freedompro lock."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(hass, api_key, device, coordinator)
|
Device(hass, api_key, device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -7,7 +7,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@ -15,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
DEVICE_CLASS_MAP = {
|
DEVICE_CLASS_MAP = {
|
||||||
"temperatureSensor": SensorDeviceClass.TEMPERATURE,
|
"temperatureSensor": SensorDeviceClass.TEMPERATURE,
|
||||||
@ -41,10 +40,12 @@ SUPPORTED_SENSORS = {"temperatureSensor", "humiditySensor", "lightSensor"}
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro sensor."""
|
"""Set up Freedompro sensor."""
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(device, coordinator)
|
Device(device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -6,7 +6,6 @@ from typing import Any
|
|||||||
from pyfreedompro import put_state
|
from pyfreedompro import put_state
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -15,15 +14,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import FreedomproDataUpdateCoordinator
|
from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FreedomproConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Freedompro switch."""
|
"""Set up Freedompro switch."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
Device(hass, api_key, device, coordinator)
|
Device(hass, api_key, device, coordinator)
|
||||||
for device in coordinator.data
|
for device in coordinator.data
|
||||||
|
@ -21,5 +21,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250204.0"]
|
"requirements": ["home-assistant-frontend==20250205.0"]
|
||||||
}
|
}
|
||||||
|
@ -14,5 +14,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["ismartgate"],
|
"loggers": ["ismartgate"],
|
||||||
"requirements": ["ismartgate==5.0.1"]
|
"requirements": ["ismartgate==5.0.2"]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ CONF_PROMPT = "prompt"
|
|||||||
|
|
||||||
CONF_RECOMMENDED = "recommended"
|
CONF_RECOMMENDED = "recommended"
|
||||||
CONF_CHAT_MODEL = "chat_model"
|
CONF_CHAT_MODEL = "chat_model"
|
||||||
RECOMMENDED_CHAT_MODEL = "models/gemini-1.5-flash-latest"
|
RECOMMENDED_CHAT_MODEL = "models/gemini-2.0-flash"
|
||||||
CONF_TEMPERATURE = "temperature"
|
CONF_TEMPERATURE = "temperature"
|
||||||
RECOMMENDED_TEMPERATURE = 1.0
|
RECOMMENDED_TEMPERATURE = 1.0
|
||||||
CONF_TOP_P = "top_p"
|
CONF_TOP_P = "top_p"
|
||||||
|
@ -38,6 +38,10 @@
|
|||||||
"local_name": "GV5126*",
|
"local_name": "GV5126*",
|
||||||
"connectable": false
|
"connectable": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"local_name": "GV5179*",
|
||||||
|
"connectable": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"local_name": "GVH5127*",
|
"local_name": "GVH5127*",
|
||||||
"connectable": false
|
"connectable": false
|
||||||
@ -131,5 +135,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
|
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["govee-ble==0.42.1"]
|
"requirements": ["govee-ble==0.43.0"]
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from govee_local_api import GoveeDevice, GoveeLightCapability
|
from govee_local_api import GoveeDevice, GoveeLightFeatures
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
@ -71,13 +71,13 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
|||||||
capabilities = device.capabilities
|
capabilities = device.capabilities
|
||||||
color_modes = {ColorMode.ONOFF}
|
color_modes = {ColorMode.ONOFF}
|
||||||
if capabilities:
|
if capabilities:
|
||||||
if GoveeLightCapability.COLOR_RGB in capabilities:
|
if GoveeLightFeatures.COLOR_RGB & capabilities.features:
|
||||||
color_modes.add(ColorMode.RGB)
|
color_modes.add(ColorMode.RGB)
|
||||||
if GoveeLightCapability.COLOR_KELVIN_TEMPERATURE in capabilities:
|
if GoveeLightFeatures.COLOR_KELVIN_TEMPERATURE & capabilities.features:
|
||||||
color_modes.add(ColorMode.COLOR_TEMP)
|
color_modes.add(ColorMode.COLOR_TEMP)
|
||||||
self._attr_max_color_temp_kelvin = 9000
|
self._attr_max_color_temp_kelvin = 9000
|
||||||
self._attr_min_color_temp_kelvin = 2000
|
self._attr_min_color_temp_kelvin = 2000
|
||||||
if GoveeLightCapability.BRIGHTNESS in capabilities:
|
if GoveeLightFeatures.BRIGHTNESS & capabilities.features:
|
||||||
color_modes.add(ColorMode.BRIGHTNESS)
|
color_modes.add(ColorMode.BRIGHTNESS)
|
||||||
|
|
||||||
self._attr_supported_color_modes = filter_supported_color_modes(color_modes)
|
self._attr_supported_color_modes = filter_supported_color_modes(color_modes)
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["network"],
|
"dependencies": ["network"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["govee-local-api==1.5.3"]
|
"requirements": ["govee-local-api==2.0.1"]
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,12 @@
|
|||||||
},
|
},
|
||||||
"elevation": {
|
"elevation": {
|
||||||
"default": "mdi:arrow-up-down"
|
"default": "mdi:arrow-up-down"
|
||||||
|
},
|
||||||
|
"total_satellites": {
|
||||||
|
"default": "mdi:satellite-variant"
|
||||||
|
},
|
||||||
|
"used_satellites": {
|
||||||
|
"default": "mdi:satellite-variant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from gps3.agps3threaded import AGPS3mechanism
|
from gps3.agps3threaded import AGPS3mechanism
|
||||||
|
|
||||||
@ -14,6 +13,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LATITUDE,
|
ATTR_LATITUDE,
|
||||||
@ -37,14 +37,32 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
ATTR_CLIMB = "climb"
|
ATTR_CLIMB = "climb"
|
||||||
ATTR_ELEVATION = "elevation"
|
ATTR_ELEVATION = "elevation"
|
||||||
ATTR_GPS_TIME = "gps_time"
|
|
||||||
ATTR_SPEED = "speed"
|
ATTR_SPEED = "speed"
|
||||||
|
ATTR_TOTAL_SATELLITES = "total_satellites"
|
||||||
|
ATTR_USED_SATELLITES = "used_satellites"
|
||||||
|
|
||||||
DEFAULT_NAME = "GPS"
|
DEFAULT_NAME = "GPS"
|
||||||
|
|
||||||
_MODE_VALUES = {2: "2d_fix", 3: "3d_fix"}
|
_MODE_VALUES = {2: "2d_fix", 3: "3d_fix"}
|
||||||
|
|
||||||
|
|
||||||
|
def count_total_satellites_fn(agps_thread: AGPS3mechanism) -> int | None:
|
||||||
|
"""Count the number of total satellites."""
|
||||||
|
satellites = agps_thread.data_stream.satellites
|
||||||
|
return None if satellites == "n/a" else len(satellites)
|
||||||
|
|
||||||
|
|
||||||
|
def count_used_satellites_fn(agps_thread: AGPS3mechanism) -> int | None:
|
||||||
|
"""Count the number of used satellites."""
|
||||||
|
satellites = agps_thread.data_stream.satellites
|
||||||
|
if satellites == "n/a":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return sum(
|
||||||
|
1 for sat in satellites if isinstance(sat, dict) and sat.get("used", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class GpsdSensorDescription(SensorEntityDescription):
|
class GpsdSensorDescription(SensorEntityDescription):
|
||||||
"""Class describing GPSD sensor entities."""
|
"""Class describing GPSD sensor entities."""
|
||||||
@ -116,6 +134,22 @@ SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = (
|
|||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
|
GpsdSensorDescription(
|
||||||
|
key=ATTR_TOTAL_SATELLITES,
|
||||||
|
translation_key=ATTR_TOTAL_SATELLITES,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=count_total_satellites_fn,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
GpsdSensorDescription(
|
||||||
|
key=ATTR_USED_SATELLITES,
|
||||||
|
translation_key=ATTR_USED_SATELLITES,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=count_used_satellites_fn,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -165,21 +199,3 @@ class GpsdSensor(SensorEntity):
|
|||||||
"""Return the state of GPSD."""
|
"""Return the state of GPSD."""
|
||||||
value = self.entity_description.value_fn(self.agps_thread)
|
value = self.entity_description.value_fn(self.agps_thread)
|
||||||
return None if value == "n/a" else value
|
return None if value == "n/a" else value
|
||||||
|
|
||||||
# Deprecated since Home Assistant 2024.9.0
|
|
||||||
# Can be removed completely in 2025.3.0
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
|
||||||
"""Return the state attributes of the GPS."""
|
|
||||||
if self.entity_description.key != ATTR_MODE:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
ATTR_LATITUDE: self.agps_thread.data_stream.lat,
|
|
||||||
ATTR_LONGITUDE: self.agps_thread.data_stream.lon,
|
|
||||||
ATTR_ELEVATION: self.agps_thread.data_stream.alt,
|
|
||||||
ATTR_GPS_TIME: self.agps_thread.data_stream.time,
|
|
||||||
ATTR_SPEED: self.agps_thread.data_stream.speed,
|
|
||||||
ATTR_CLIMB: self.agps_thread.data_stream.climb,
|
|
||||||
ATTR_MODE: self.agps_thread.data_stream.mode,
|
|
||||||
}
|
|
||||||
|
@ -50,6 +50,14 @@
|
|||||||
},
|
},
|
||||||
"mode": { "name": "[%key:common::config_flow::data::mode%]" }
|
"mode": { "name": "[%key:common::config_flow::data::mode%]" }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"total_satellites": {
|
||||||
|
"name": "Total satellites",
|
||||||
|
"unit_of_measurement": "satellites"
|
||||||
|
},
|
||||||
|
"used_satellites": {
|
||||||
|
"name": "Used satellites",
|
||||||
|
"unit_of_measurement": "satellites"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["habiticalib"],
|
"loggers": ["habiticalib"],
|
||||||
"requirements": ["habiticalib==0.3.4"]
|
"requirements": ["habiticalib==0.3.5"]
|
||||||
}
|
}
|
||||||
|
@ -510,7 +510,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
|||||||
or (task.notes and keyword in task.notes.lower())
|
or (task.notes and keyword in task.notes.lower())
|
||||||
or any(keyword in item.text.lower() for item in task.checklist)
|
or any(keyword in item.text.lower() for item in task.checklist)
|
||||||
]
|
]
|
||||||
result: dict[str, Any] = {"tasks": response}
|
result: dict[str, Any] = {"tasks": [task.to_dict() for task in response]}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
@ -20,6 +20,7 @@ from aiohasupervisor.models import (
|
|||||||
backups as supervisor_backups,
|
backups as supervisor_backups,
|
||||||
mounts as supervisor_mounts,
|
mounts as supervisor_mounts,
|
||||||
)
|
)
|
||||||
|
from aiohasupervisor.models.backups import LOCATION_CLOUD_BACKUP, LOCATION_LOCAL_STORAGE
|
||||||
|
|
||||||
from homeassistant.components.backup import (
|
from homeassistant.components.backup import (
|
||||||
DATA_MANAGER,
|
DATA_MANAGER,
|
||||||
@ -56,8 +57,6 @@ from homeassistant.util.enum import try_parse_enum
|
|||||||
from .const import DOMAIN, EVENT_SUPERVISOR_EVENT
|
from .const import DOMAIN, EVENT_SUPERVISOR_EVENT
|
||||||
from .handler import get_supervisor_client
|
from .handler import get_supervisor_client
|
||||||
|
|
||||||
LOCATION_CLOUD_BACKUP = ".cloud_backup"
|
|
||||||
LOCATION_LOCAL = ".local"
|
|
||||||
MOUNT_JOBS = ("mount_manager_create_mount", "mount_manager_remove_mount")
|
MOUNT_JOBS = ("mount_manager_create_mount", "mount_manager_remove_mount")
|
||||||
RESTORE_JOB_ID_ENV = "SUPERVISOR_RESTORE_JOB_ID"
|
RESTORE_JOB_ID_ENV = "SUPERVISOR_RESTORE_JOB_ID"
|
||||||
# Set on backups automatically created when updating an addon
|
# Set on backups automatically created when updating an addon
|
||||||
@ -72,7 +71,9 @@ async def async_get_backup_agents(
|
|||||||
"""Return the hassio backup agents."""
|
"""Return the hassio backup agents."""
|
||||||
client = get_supervisor_client(hass)
|
client = get_supervisor_client(hass)
|
||||||
mounts = await client.mounts.info()
|
mounts = await client.mounts.info()
|
||||||
agents: list[BackupAgent] = [SupervisorBackupAgent(hass, "local", None)]
|
agents: list[BackupAgent] = [
|
||||||
|
SupervisorBackupAgent(hass, "local", LOCATION_LOCAL_STORAGE)
|
||||||
|
]
|
||||||
for mount in mounts.mounts:
|
for mount in mounts.mounts:
|
||||||
if mount.usage is not supervisor_mounts.MountUsage.BACKUP:
|
if mount.usage is not supervisor_mounts.MountUsage.BACKUP:
|
||||||
continue
|
continue
|
||||||
@ -112,7 +113,7 @@ def async_register_backup_agents_listener(
|
|||||||
|
|
||||||
|
|
||||||
def _backup_details_to_agent_backup(
|
def _backup_details_to_agent_backup(
|
||||||
details: supervisor_backups.BackupComplete, location: str | None
|
details: supervisor_backups.BackupComplete, location: str
|
||||||
) -> AgentBackup:
|
) -> AgentBackup:
|
||||||
"""Convert a supervisor backup details object to an agent backup."""
|
"""Convert a supervisor backup details object to an agent backup."""
|
||||||
homeassistant_included = details.homeassistant is not None
|
homeassistant_included = details.homeassistant is not None
|
||||||
@ -125,7 +126,6 @@ def _backup_details_to_agent_backup(
|
|||||||
for addon in details.addons
|
for addon in details.addons
|
||||||
]
|
]
|
||||||
extra_metadata = details.extra or {}
|
extra_metadata = details.extra or {}
|
||||||
location = location or LOCATION_LOCAL
|
|
||||||
return AgentBackup(
|
return AgentBackup(
|
||||||
addons=addons,
|
addons=addons,
|
||||||
backup_id=details.slug,
|
backup_id=details.slug,
|
||||||
@ -148,7 +148,7 @@ class SupervisorBackupAgent(BackupAgent):
|
|||||||
|
|
||||||
domain = DOMAIN
|
domain = DOMAIN
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, name: str, location: str | None) -> None:
|
def __init__(self, hass: HomeAssistant, name: str, location: str) -> None:
|
||||||
"""Initialize the backup agent."""
|
"""Initialize the backup agent."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
@ -206,7 +206,7 @@ class SupervisorBackupAgent(BackupAgent):
|
|||||||
backup_list = await self._client.backups.list()
|
backup_list = await self._client.backups.list()
|
||||||
result = []
|
result = []
|
||||||
for backup in backup_list:
|
for backup in backup_list:
|
||||||
if not backup.locations or self.location not in backup.locations:
|
if self.location not in backup.location_attributes:
|
||||||
continue
|
continue
|
||||||
details = await self._client.backups.backup_info(backup.slug)
|
details = await self._client.backups.backup_info(backup.slug)
|
||||||
result.append(_backup_details_to_agent_backup(details, self.location))
|
result.append(_backup_details_to_agent_backup(details, self.location))
|
||||||
@ -222,7 +222,7 @@ class SupervisorBackupAgent(BackupAgent):
|
|||||||
details = await self._client.backups.backup_info(backup_id)
|
details = await self._client.backups.backup_info(backup_id)
|
||||||
except SupervisorNotFoundError:
|
except SupervisorNotFoundError:
|
||||||
return None
|
return None
|
||||||
if self.location not in details.locations:
|
if self.location not in details.location_attributes:
|
||||||
return None
|
return None
|
||||||
return _backup_details_to_agent_backup(details, self.location)
|
return _backup_details_to_agent_backup(details, self.location)
|
||||||
|
|
||||||
@ -295,8 +295,8 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
# will be handled by async_upload_backup.
|
# will be handled by async_upload_backup.
|
||||||
# If the lists are the same length, it does not matter which one we send,
|
# If the lists are the same length, it does not matter which one we send,
|
||||||
# we send the encrypted list to have a well defined behavior.
|
# we send the encrypted list to have a well defined behavior.
|
||||||
encrypted_locations: list[str | None] = []
|
encrypted_locations: list[str] = []
|
||||||
decrypted_locations: list[str | None] = []
|
decrypted_locations: list[str] = []
|
||||||
agents_settings = manager.config.data.agents
|
agents_settings = manager.config.data.agents
|
||||||
for hassio_agent in hassio_agents:
|
for hassio_agent in hassio_agents:
|
||||||
if password is not None:
|
if password is not None:
|
||||||
@ -353,12 +353,12 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
eager_start=False, # To ensure the task is not started before we return
|
eager_start=False, # To ensure the task is not started before we return
|
||||||
)
|
)
|
||||||
|
|
||||||
return (NewBackup(backup_job_id=backup.job_id), backup_task)
|
return (NewBackup(backup_job_id=backup.job_id.hex), backup_task)
|
||||||
|
|
||||||
async def _async_wait_for_backup(
|
async def _async_wait_for_backup(
|
||||||
self,
|
self,
|
||||||
backup: supervisor_backups.NewBackup,
|
backup: supervisor_backups.NewBackup,
|
||||||
locations: list[str | None],
|
locations: list[str],
|
||||||
*,
|
*,
|
||||||
on_progress: Callable[[CreateBackupEvent], None],
|
on_progress: Callable[[CreateBackupEvent], None],
|
||||||
remove_after_upload: bool,
|
remove_after_upload: bool,
|
||||||
@ -508,7 +508,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
restore_location: str | None
|
restore_location: str
|
||||||
if manager.backup_agents[agent_id].domain != DOMAIN:
|
if manager.backup_agents[agent_id].domain != DOMAIN:
|
||||||
# Download the backup to the supervisor. Supervisor will clean up the backup
|
# Download the backup to the supervisor. Supervisor will clean up the backup
|
||||||
# two days after the restore is done.
|
# two days after the restore is done.
|
||||||
@ -577,10 +577,11 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
on_progress: Callable[[RestoreBackupEvent | IdleEvent], None],
|
on_progress: Callable[[RestoreBackupEvent | IdleEvent], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check restore status after core restart."""
|
"""Check restore status after core restart."""
|
||||||
if not (restore_job_id := os.environ.get(RESTORE_JOB_ID_ENV)):
|
if not (restore_job_str := os.environ.get(RESTORE_JOB_ID_ENV)):
|
||||||
_LOGGER.debug("No restore job ID found in environment")
|
_LOGGER.debug("No restore job ID found in environment")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
restore_job_id = UUID(restore_job_str)
|
||||||
_LOGGER.debug("Found restore job ID %s in environment", restore_job_id)
|
_LOGGER.debug("Found restore job ID %s in environment", restore_job_id)
|
||||||
|
|
||||||
sent_event = False
|
sent_event = False
|
||||||
@ -634,7 +635,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_listen_job_events(
|
def _async_listen_job_events(
|
||||||
self, job_id: str, on_event: Callable[[Mapping[str, Any]], None]
|
self, job_id: UUID, on_event: Callable[[Mapping[str, Any]], None]
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
"""Listen for job events."""
|
"""Listen for job events."""
|
||||||
|
|
||||||
@ -649,7 +650,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
if (
|
if (
|
||||||
data.get("event") != "job"
|
data.get("event") != "job"
|
||||||
or not (event_data := data.get("data"))
|
or not (event_data := data.get("data"))
|
||||||
or event_data.get("uuid") != job_id
|
or event_data.get("uuid") != job_id.hex
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
on_event(event_data)
|
on_event(event_data)
|
||||||
@ -660,10 +661,10 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
return unsub
|
return unsub
|
||||||
|
|
||||||
async def _get_job_state(
|
async def _get_job_state(
|
||||||
self, job_id: str, on_event: Callable[[Mapping[str, Any]], None]
|
self, job_id: UUID, on_event: Callable[[Mapping[str, Any]], None]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Poll a job for its state."""
|
"""Poll a job for its state."""
|
||||||
job = await self._client.jobs.get_job(UUID(job_id))
|
job = await self._client.jobs.get_job(job_id)
|
||||||
_LOGGER.debug("Job state: %s", job)
|
_LOGGER.debug("Job state: %s", job)
|
||||||
on_event(job.to_dict())
|
on_event(job.to_dict())
|
||||||
|
|
||||||
|
@ -295,6 +295,8 @@ def async_remove_addons_from_dev_reg(
|
|||||||
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Class to retrieve Hass.io status."""
|
"""Class to retrieve Hass.io status."""
|
||||||
|
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry
|
self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -302,6 +304,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||||
# We don't want an immediate refresh since we want to avoid
|
# We don't want an immediate refresh since we want to avoid
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["aiohasupervisor==0.2.2b6"],
|
"requirements": ["aiohasupervisor==0.3.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@ -82,12 +82,20 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
|
|||||||
try:
|
try:
|
||||||
await self.heos.connect()
|
await self.heos.connect()
|
||||||
except HeosError as error:
|
except HeosError as error:
|
||||||
raise ConfigEntryNotReady from error
|
_LOGGER.debug("Unable to connect to %s", self.host, exc_info=True)
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="unable_to_connect",
|
||||||
|
translation_placeholders={"host": self.host},
|
||||||
|
) from error
|
||||||
# Load players
|
# Load players
|
||||||
try:
|
try:
|
||||||
await self.heos.get_players()
|
await self.heos.get_players()
|
||||||
except HeosError as error:
|
except HeosError as error:
|
||||||
raise ConfigEntryNotReady from error
|
_LOGGER.debug("Unexpected error retrieving players", exc_info=True)
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN, translation_key="unable_to_get_players"
|
||||||
|
) from error
|
||||||
|
|
||||||
if not self.heos.is_signed_in:
|
if not self.heos.is_signed_in:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
@ -54,7 +54,7 @@ rules:
|
|||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: todo
|
exception-translations: done
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: done
|
reconfiguration-flow: done
|
||||||
repair-issues: todo
|
repair-issues: todo
|
||||||
|
@ -112,6 +112,12 @@
|
|||||||
"not_heos_media_player": {
|
"not_heos_media_player": {
|
||||||
"message": "Entity {entity_id} is not a HEOS media player entity"
|
"message": "Entity {entity_id} is not a HEOS media player entity"
|
||||||
},
|
},
|
||||||
|
"unable_to_connect": {
|
||||||
|
"message": "Unable to connect to {host}"
|
||||||
|
},
|
||||||
|
"unable_to_get_players": {
|
||||||
|
"message": "Unexpected error retrieving players"
|
||||||
|
},
|
||||||
"unknown_source": {
|
"unknown_source": {
|
||||||
"message": "Unknown source: {source}"
|
"message": "Unknown source: {source}"
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,11 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"name": "[%key:common::config_flow::data::name%]",
|
"name": "[%key:common::config_flow::data::name%]",
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
"mode": "Travel Mode"
|
"mode": "Travel mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"origin_menu": {
|
"origin_menu": {
|
||||||
"title": "Choose Origin",
|
"title": "Choose origin",
|
||||||
"menu_options": {
|
"menu_options": {
|
||||||
"origin_coordinates": "Using a map location",
|
"origin_coordinates": "Using a map location",
|
||||||
"origin_entity": "Using an entity"
|
"origin_entity": "Using an entity"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"destination_menu": {
|
"destination_menu": {
|
||||||
"title": "Choose Destination",
|
"title": "Choose destination",
|
||||||
"menu_options": {
|
"menu_options": {
|
||||||
"destination_coordinates": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_coordinates%]",
|
"destination_coordinates": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_coordinates%]",
|
||||||
"destination_entity": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_entity%]"
|
"destination_entity": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_entity%]"
|
||||||
@ -60,13 +60,13 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"traffic_mode": "Traffic Mode",
|
"traffic_mode": "Traffic mode",
|
||||||
"route_mode": "Route Mode",
|
"route_mode": "Route mode",
|
||||||
"unit_system": "Unit system"
|
"unit_system": "Unit system"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time_menu": {
|
"time_menu": {
|
||||||
"title": "Choose Time Type",
|
"title": "Choose time type",
|
||||||
"menu_options": {
|
"menu_options": {
|
||||||
"departure_time": "Configure a departure time",
|
"departure_time": "Configure a departure time",
|
||||||
"arrival_time": "Configure an arrival time",
|
"arrival_time": "Configure an arrival time",
|
||||||
@ -74,15 +74,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"departure_time": {
|
"departure_time": {
|
||||||
"title": "Choose Departure Time",
|
"title": "Choose departure time",
|
||||||
"data": {
|
"data": {
|
||||||
"departure_time": "Departure Time"
|
"departure_time": "Departure time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"arrival_time": {
|
"arrival_time": {
|
||||||
"title": "Choose Arrival Time",
|
"title": "Choose arrival time",
|
||||||
"data": {
|
"data": {
|
||||||
"arrival_time": "Arrival Time"
|
"arrival_time": "Arrival time"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["holidays==0.65", "babel==2.15.0"]
|
"requirements": ["holidays==0.66", "babel==2.15.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,15 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
DOMAIN = "homeassistant_hardware"
|
from .const import DATA_COMPONENT, DOMAIN
|
||||||
|
from .helpers import HardwareInfoDispatcher
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the component."""
|
"""Set up the component."""
|
||||||
|
|
||||||
|
hass.data[DATA_COMPONENT] = HardwareInfoDispatcher(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
"""Constants for the Homeassistant Hardware integration."""
|
"""Constants for the Homeassistant Hardware integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .helpers import HardwareInfoDispatcher
|
||||||
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
DOMAIN = "homeassistant_hardware"
|
||||||
|
DATA_COMPONENT: HassKey[HardwareInfoDispatcher] = HassKey(DOMAIN)
|
||||||
|
|
||||||
ZHA_DOMAIN = "zha"
|
ZHA_DOMAIN = "zha"
|
||||||
|
OTBR_DOMAIN = "otbr"
|
||||||
|
|
||||||
OTBR_ADDON_NAME = "OpenThread Border Router"
|
OTBR_ADDON_NAME = "OpenThread Border Router"
|
||||||
OTBR_ADDON_MANAGER_DATA = "openthread_border_router"
|
OTBR_ADDON_MANAGER_DATA = "openthread_border_router"
|
||||||
|
@ -25,12 +25,14 @@ from homeassistant.data_entry_flow import AbortFlow
|
|||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
|
|
||||||
from . import silabs_multiprotocol_addon
|
from . import silabs_multiprotocol_addon
|
||||||
from .const import ZHA_DOMAIN
|
from .const import OTBR_DOMAIN, ZHA_DOMAIN
|
||||||
from .util import (
|
from .util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
|
OwningAddon,
|
||||||
|
OwningIntegration,
|
||||||
get_otbr_addon_manager,
|
get_otbr_addon_manager,
|
||||||
get_zha_device_path,
|
|
||||||
get_zigbee_flasher_addon_manager,
|
get_zigbee_flasher_addon_manager,
|
||||||
|
guess_hardware_owners,
|
||||||
probe_silabs_firmware_type,
|
probe_silabs_firmware_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -519,19 +521,15 @@ class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Pick Zigbee firmware."""
|
"""Pick Zigbee firmware."""
|
||||||
assert self._device is not None
|
assert self._device is not None
|
||||||
|
owners = await guess_hardware_owners(self.hass, self._device)
|
||||||
|
|
||||||
if is_hassio(self.hass):
|
for info in owners:
|
||||||
otbr_manager = get_otbr_addon_manager(self.hass)
|
for owner in info.owners:
|
||||||
otbr_addon_info = await self._async_get_addon_info(otbr_manager)
|
if info.source == OTBR_DOMAIN and isinstance(owner, OwningAddon):
|
||||||
|
raise AbortFlow(
|
||||||
if (
|
"otbr_still_using_stick",
|
||||||
otbr_addon_info.state != AddonState.NOT_INSTALLED
|
description_placeholders=self._get_translation_placeholders(),
|
||||||
and otbr_addon_info.options.get("device") == self._device
|
)
|
||||||
):
|
|
||||||
raise AbortFlow(
|
|
||||||
"otbr_still_using_stick",
|
|
||||||
description_placeholders=self._get_translation_placeholders(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return await super().async_step_pick_firmware_zigbee(user_input)
|
return await super().async_step_pick_firmware_zigbee(user_input)
|
||||||
|
|
||||||
@ -541,15 +539,14 @@ class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
|
|||||||
"""Pick Thread firmware."""
|
"""Pick Thread firmware."""
|
||||||
assert self._device is not None
|
assert self._device is not None
|
||||||
|
|
||||||
for zha_entry in self.hass.config_entries.async_entries(
|
owners = await guess_hardware_owners(self.hass, self._device)
|
||||||
ZHA_DOMAIN,
|
|
||||||
include_ignore=False,
|
for info in owners:
|
||||||
include_disabled=True,
|
for owner in info.owners:
|
||||||
):
|
if info.source == ZHA_DOMAIN and isinstance(owner, OwningIntegration):
|
||||||
if get_zha_device_path(zha_entry) == self._device:
|
raise AbortFlow(
|
||||||
raise AbortFlow(
|
"zha_still_using_stick",
|
||||||
"zha_still_using_stick",
|
description_placeholders=self._get_translation_placeholders(),
|
||||||
description_placeholders=self._get_translation_placeholders(),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return await super().async_step_pick_firmware_thread(user_input)
|
return await super().async_step_pick_firmware_thread(user_input)
|
||||||
|
143
homeassistant/components/homeassistant_hardware/helpers.py
Normal file
143
homeassistant/components/homeassistant_hardware/helpers.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""Home Assistant Hardware integration helpers."""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||||
|
import logging
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||||
|
|
||||||
|
from . import DATA_COMPONENT
|
||||||
|
from .util import FirmwareInfo
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SyncHardwareFirmwareInfoModule(Protocol):
|
||||||
|
"""Protocol type for Home Assistant Hardware firmware info platform modules."""
|
||||||
|
|
||||||
|
def get_firmware_info(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
) -> FirmwareInfo | None:
|
||||||
|
"""Return radio firmware information for the config entry, synchronously."""
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncHardwareFirmwareInfoModule(Protocol):
|
||||||
|
"""Protocol type for Home Assistant Hardware firmware info platform modules."""
|
||||||
|
|
||||||
|
async def async_get_firmware_info(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
) -> FirmwareInfo | None:
|
||||||
|
"""Return radio firmware information for the config entry, asynchronously."""
|
||||||
|
|
||||||
|
|
||||||
|
type HardwareFirmwareInfoModule = (
|
||||||
|
SyncHardwareFirmwareInfoModule | AsyncHardwareFirmwareInfoModule
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HardwareInfoDispatcher:
|
||||||
|
"""Central dispatcher for hardware/firmware information."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the dispatcher."""
|
||||||
|
self.hass = hass
|
||||||
|
self._providers: dict[str, HardwareFirmwareInfoModule] = {}
|
||||||
|
self._notification_callbacks: defaultdict[
|
||||||
|
str, set[Callable[[FirmwareInfo], None]]
|
||||||
|
] = defaultdict(set)
|
||||||
|
|
||||||
|
def register_firmware_info_provider(
|
||||||
|
self, domain: str, platform: HardwareFirmwareInfoModule
|
||||||
|
) -> None:
|
||||||
|
"""Register a firmware info provider."""
|
||||||
|
if domain in self._providers:
|
||||||
|
raise ValueError(
|
||||||
|
f"Domain {domain} is already registered as a firmware info provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
# There is no need to handle "unregistration" because integrations cannot be
|
||||||
|
# wholly removed at runtime
|
||||||
|
self._providers[domain] = platform
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Registered firmware info provider from domain %r: %s", domain, platform
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_firmware_info_callback(
|
||||||
|
self, device: str, callback: Callable[[FirmwareInfo], None]
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Register a firmware info notification callback."""
|
||||||
|
self._notification_callbacks[device].add(callback)
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_remove_callback() -> None:
|
||||||
|
self._notification_callbacks[device].discard(callback)
|
||||||
|
|
||||||
|
return async_remove_callback
|
||||||
|
|
||||||
|
async def notify_firmware_info(
|
||||||
|
self, domain: str, firmware_info: FirmwareInfo
|
||||||
|
) -> None:
|
||||||
|
"""Notify the dispatcher of new firmware information."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Received firmware info notification from %r: %s", domain, firmware_info
|
||||||
|
)
|
||||||
|
|
||||||
|
for callback in self._notification_callbacks.get(firmware_info.device, []):
|
||||||
|
try:
|
||||||
|
callback(firmware_info)
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Error while notifying firmware info listener %s", callback
|
||||||
|
)
|
||||||
|
|
||||||
|
async def iter_firmware_info(self) -> AsyncIterator[FirmwareInfo]:
|
||||||
|
"""Iterate over all firmware information for all hardware."""
|
||||||
|
for domain, fw_info_module in self._providers.items():
|
||||||
|
for config_entry in self.hass.config_entries.async_entries(domain):
|
||||||
|
try:
|
||||||
|
if hasattr(fw_info_module, "get_firmware_info"):
|
||||||
|
fw_info = fw_info_module.get_firmware_info(
|
||||||
|
self.hass, config_entry
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fw_info = await fw_info_module.async_get_firmware_info(
|
||||||
|
self.hass, config_entry
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Error while getting firmware info from %r", fw_info_module
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if fw_info is not None:
|
||||||
|
yield fw_info
|
||||||
|
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_register_firmware_info_provider(
|
||||||
|
hass: HomeAssistant, domain: str, platform: HardwareFirmwareInfoModule
|
||||||
|
) -> None:
|
||||||
|
"""Register a firmware info provider."""
|
||||||
|
return hass.data[DATA_COMPONENT].register_firmware_info_provider(domain, platform)
|
||||||
|
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_register_firmware_info_callback(
|
||||||
|
hass: HomeAssistant, device: str, callback: Callable[[FirmwareInfo], None]
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Register a firmware info provider."""
|
||||||
|
return hass.data[DATA_COMPONENT].register_firmware_info_callback(device, callback)
|
||||||
|
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_notify_firmware_info(
|
||||||
|
hass: HomeAssistant, domain: str, firmware_info: FirmwareInfo
|
||||||
|
) -> Awaitable[None]:
|
||||||
|
"""Notify the dispatcher of new firmware information."""
|
||||||
|
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info)
|
@ -2,27 +2,27 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
import logging
|
import logging
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||||
from universal_silabs_flasher.flasher import Flasher
|
from universal_silabs_flasher.flasher import Flasher
|
||||||
|
|
||||||
from homeassistant.components.hassio import AddonError, AddonState
|
from homeassistant.components.hassio import AddonError, AddonState
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
|
|
||||||
|
from . import DATA_COMPONENT
|
||||||
from .const import (
|
from .const import (
|
||||||
OTBR_ADDON_MANAGER_DATA,
|
OTBR_ADDON_MANAGER_DATA,
|
||||||
OTBR_ADDON_NAME,
|
OTBR_ADDON_NAME,
|
||||||
OTBR_ADDON_SLUG,
|
OTBR_ADDON_SLUG,
|
||||||
ZHA_DOMAIN,
|
|
||||||
ZIGBEE_FLASHER_ADDON_MANAGER_DATA,
|
ZIGBEE_FLASHER_ADDON_MANAGER_DATA,
|
||||||
ZIGBEE_FLASHER_ADDON_NAME,
|
ZIGBEE_FLASHER_ADDON_NAME,
|
||||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||||
@ -55,11 +55,6 @@ class ApplicationType(StrEnum):
|
|||||||
return FlasherApplicationType(self.value)
|
return FlasherApplicationType(self.value)
|
||||||
|
|
||||||
|
|
||||||
def get_zha_device_path(config_entry: ConfigEntry) -> str | None:
|
|
||||||
"""Get the device path from a ZHA config entry."""
|
|
||||||
return cast(str | None, config_entry.data.get("device", {}).get("path", None))
|
|
||||||
|
|
||||||
|
|
||||||
@singleton(OTBR_ADDON_MANAGER_DATA)
|
@singleton(OTBR_ADDON_MANAGER_DATA)
|
||||||
@callback
|
@callback
|
||||||
def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||||
@ -84,31 +79,80 @@ def get_zigbee_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
class FirmwareGuess:
|
class OwningAddon:
|
||||||
|
"""Owning add-on."""
|
||||||
|
|
||||||
|
slug: str
|
||||||
|
|
||||||
|
def _get_addon_manager(self, hass: HomeAssistant) -> WaitingAddonManager:
|
||||||
|
return WaitingAddonManager(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
f"Add-on {self.slug}",
|
||||||
|
self.slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def is_running(self, hass: HomeAssistant) -> bool:
|
||||||
|
"""Check if the add-on is running."""
|
||||||
|
addon_manager = self._get_addon_manager(hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
addon_info = await addon_manager.async_get_addon_info()
|
||||||
|
except AddonError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return addon_info.state == AddonState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class OwningIntegration:
|
||||||
|
"""Owning integration."""
|
||||||
|
|
||||||
|
config_entry_id: str
|
||||||
|
|
||||||
|
async def is_running(self, hass: HomeAssistant) -> bool:
|
||||||
|
"""Check if the integration is running."""
|
||||||
|
if (entry := hass.config_entries.async_get_entry(self.config_entry_id)) is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return entry.state in (
|
||||||
|
ConfigEntryState.LOADED,
|
||||||
|
ConfigEntryState.SETUP_RETRY,
|
||||||
|
ConfigEntryState.SETUP_IN_PROGRESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class FirmwareInfo:
|
||||||
"""Firmware guess."""
|
"""Firmware guess."""
|
||||||
|
|
||||||
is_running: bool
|
device: str
|
||||||
firmware_type: ApplicationType
|
firmware_type: ApplicationType
|
||||||
|
firmware_version: str | None
|
||||||
|
|
||||||
source: str
|
source: str
|
||||||
|
owners: list[OwningAddon | OwningIntegration]
|
||||||
|
|
||||||
|
async def is_running(self, hass: HomeAssistant) -> bool:
|
||||||
|
"""Check if the firmware owner is running."""
|
||||||
|
states = await asyncio.gather(*(o.is_running(hass) for o in self.owners))
|
||||||
|
if not states:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(states)
|
||||||
|
|
||||||
|
|
||||||
async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> FirmwareGuess:
|
async def guess_hardware_owners(
|
||||||
"""Guess the firmware type based on installed addons and other integrations."""
|
hass: HomeAssistant, device_path: str
|
||||||
device_guesses: defaultdict[str | None, list[FirmwareGuess]] = defaultdict(list)
|
) -> list[FirmwareInfo]:
|
||||||
|
"""Guess the firmware info based on installed addons and other integrations."""
|
||||||
|
device_guesses: defaultdict[str, list[FirmwareInfo]] = defaultdict(list)
|
||||||
|
|
||||||
for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN):
|
async for firmware_info in hass.data[DATA_COMPONENT].iter_firmware_info():
|
||||||
zha_path = get_zha_device_path(zha_config_entry)
|
device_guesses[firmware_info.device].append(firmware_info)
|
||||||
|
|
||||||
if zha_path is not None:
|
|
||||||
device_guesses[zha_path].append(
|
|
||||||
FirmwareGuess(
|
|
||||||
is_running=(zha_config_entry.state == ConfigEntryState.LOADED),
|
|
||||||
firmware_type=ApplicationType.EZSP,
|
|
||||||
source="zha",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# It may be possible for the OTBR addon to be present without the integration
|
||||||
if is_hassio(hass):
|
if is_hassio(hass):
|
||||||
otbr_addon_manager = get_otbr_addon_manager(hass)
|
otbr_addon_manager = get_otbr_addon_manager(hass)
|
||||||
|
|
||||||
@ -119,14 +163,22 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware
|
|||||||
else:
|
else:
|
||||||
if otbr_addon_info.state != AddonState.NOT_INSTALLED:
|
if otbr_addon_info.state != AddonState.NOT_INSTALLED:
|
||||||
otbr_path = otbr_addon_info.options.get("device")
|
otbr_path = otbr_addon_info.options.get("device")
|
||||||
device_guesses[otbr_path].append(
|
|
||||||
FirmwareGuess(
|
|
||||||
is_running=(otbr_addon_info.state == AddonState.RUNNING),
|
|
||||||
firmware_type=ApplicationType.SPINEL,
|
|
||||||
source="otbr",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Only create a new entry if there are no existing OTBR ones
|
||||||
|
if otbr_path is not None and not any(
|
||||||
|
info.source == "otbr" for info in device_guesses[otbr_path]
|
||||||
|
):
|
||||||
|
device_guesses[otbr_path].append(
|
||||||
|
FirmwareInfo(
|
||||||
|
device=otbr_path,
|
||||||
|
firmware_type=ApplicationType.SPINEL,
|
||||||
|
firmware_version=None,
|
||||||
|
source="otbr",
|
||||||
|
owners=[OwningAddon(slug=otbr_addon_manager.addon_slug)],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_hassio(hass):
|
||||||
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -136,30 +188,48 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware
|
|||||||
else:
|
else:
|
||||||
if multipan_addon_info.state != AddonState.NOT_INSTALLED:
|
if multipan_addon_info.state != AddonState.NOT_INSTALLED:
|
||||||
multipan_path = multipan_addon_info.options.get("device")
|
multipan_path = multipan_addon_info.options.get("device")
|
||||||
device_guesses[multipan_path].append(
|
|
||||||
FirmwareGuess(
|
|
||||||
is_running=(multipan_addon_info.state == AddonState.RUNNING),
|
|
||||||
firmware_type=ApplicationType.CPC,
|
|
||||||
source="multiprotocol",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fall back to EZSP if we can't guess the firmware type
|
if multipan_path is not None:
|
||||||
if device_path not in device_guesses:
|
device_guesses[multipan_path].append(
|
||||||
return FirmwareGuess(
|
FirmwareInfo(
|
||||||
is_running=False, firmware_type=ApplicationType.EZSP, source="unknown"
|
device=multipan_path,
|
||||||
|
firmware_type=ApplicationType.CPC,
|
||||||
|
firmware_version=None,
|
||||||
|
source="multiprotocol",
|
||||||
|
owners=[
|
||||||
|
OwningAddon(slug=multipan_addon_manager.addon_slug)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return device_guesses.get(device_path, [])
|
||||||
|
|
||||||
|
|
||||||
|
async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> FirmwareInfo:
|
||||||
|
"""Guess the firmware type based on installed addons and other integrations."""
|
||||||
|
|
||||||
|
hardware_owners = await guess_hardware_owners(hass, device_path)
|
||||||
|
|
||||||
|
# Fall back to EZSP if we have no way to guess
|
||||||
|
if not hardware_owners:
|
||||||
|
return FirmwareInfo(
|
||||||
|
device=device_path,
|
||||||
|
firmware_type=ApplicationType.EZSP,
|
||||||
|
firmware_version=None,
|
||||||
|
source="unknown",
|
||||||
|
owners=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prioritizes guesses that were pulled from a running addon or integration but keep
|
# Prioritize guesses that are pulled from a real source
|
||||||
# the sort order we defined above
|
guesses = [
|
||||||
guesses = sorted(
|
(guess, sum([await owner.is_running(hass) for owner in guess.owners]))
|
||||||
device_guesses[device_path],
|
for guess in hardware_owners
|
||||||
key=lambda guess: guess.is_running,
|
]
|
||||||
)
|
guesses.sort(key=lambda p: p[1])
|
||||||
|
|
||||||
assert guesses
|
assert guesses
|
||||||
|
|
||||||
return guesses[-1]
|
# Pick the best one. We use a stable sort so ZHA < OTBR < multi-PAN
|
||||||
|
return guesses[-1][0]
|
||||||
|
|
||||||
|
|
||||||
async def probe_silabs_firmware_type(
|
async def probe_silabs_firmware_type(
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_type
|
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
# Add-on startup with type service get started before Core, always (e.g. the
|
# Add-on startup with type service get started before Core, always (e.g. the
|
||||||
# Multi-Protocol add-on). Probing the firmware would interfere with the add-on,
|
# Multi-Protocol add-on). Probing the firmware would interfere with the add-on,
|
||||||
# so we can't safely probe here. Instead, we must make an educated guess!
|
# so we can't safely probe here. Instead, we must make an educated guess!
|
||||||
firmware_guess = await guess_firmware_type(
|
firmware_guess = await guess_firmware_info(
|
||||||
hass, config_entry.data["device"]
|
hass, config_entry.data["device"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
|||||||
)
|
)
|
||||||
from homeassistant.components.homeassistant_hardware.util import (
|
from homeassistant.components.homeassistant_hardware.util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
guess_firmware_type,
|
guess_firmware_info,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry
|
from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -75,7 +75,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
# Add-on startup with type service get started before Core, always (e.g. the
|
# Add-on startup with type service get started before Core, always (e.g. the
|
||||||
# Multi-Protocol add-on). Probing the firmware would interfere with the add-on,
|
# Multi-Protocol add-on). Probing the firmware would interfere with the add-on,
|
||||||
# so we can't safely probe here. Instead, we must make an educated guess!
|
# so we can't safely probe here. Instead, we must make an educated guess!
|
||||||
firmware_guess = await guess_firmware_type(hass, RADIO_DEVICE)
|
firmware_guess = await guess_firmware_info(hass, RADIO_DEVICE)
|
||||||
|
|
||||||
new_data = {**config_entry.data}
|
new_data = {**config_entry.data}
|
||||||
new_data[FIRMWARE] = firmware_guess.firmware_type.value
|
new_data[FIRMWARE] = firmware_guess.firmware_type.value
|
||||||
|
@ -19,6 +19,7 @@ from homeassistant.components.sensor import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_VIA_DEVICE,
|
ATTR_VIA_DEVICE,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
UnitOfApparentPower,
|
UnitOfApparentPower,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
@ -137,6 +138,21 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
HomeWizardSensorEntityDescription(
|
||||||
|
key="wifi_rssi",
|
||||||
|
translation_key="wifi_rssi",
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
has_fn=(
|
||||||
|
lambda data: data.system is not None
|
||||||
|
and data.system.wifi_rssi_db is not None
|
||||||
|
),
|
||||||
|
value_fn=(
|
||||||
|
lambda data: data.system.wifi_rssi_db if data.system is not None else None
|
||||||
|
),
|
||||||
|
),
|
||||||
HomeWizardSensorEntityDescription(
|
HomeWizardSensorEntityDescription(
|
||||||
key="total_power_import_kwh",
|
key="total_power_import_kwh",
|
||||||
translation_key="total_energy_import_kwh",
|
translation_key="total_energy_import_kwh",
|
||||||
|
@ -78,6 +78,9 @@
|
|||||||
"wifi_strength": {
|
"wifi_strength": {
|
||||||
"name": "Wi-Fi strength"
|
"name": "Wi-Fi strength"
|
||||||
},
|
},
|
||||||
|
"wifi_rssi": {
|
||||||
|
"name": "Wi-Fi RSSI"
|
||||||
|
},
|
||||||
"total_energy_import_kwh": {
|
"total_energy_import_kwh": {
|
||||||
"name": "Energy import"
|
"name": "Energy import"
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"title": "Connect to the PowerView Hub",
|
"title": "Connect to the PowerView Hub",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::ip%]",
|
"host": "[%key:common::config_flow::data::ip%]",
|
||||||
"api_version": "Hub Generation"
|
"api_version": "Hub generation"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"api_version": "API version is detectable, but you can override and force a specific version"
|
"api_version": "API version is detectable, but you can override and force a specific version"
|
||||||
@ -19,7 +19,7 @@
|
|||||||
"flow_title": "{name} ({host})",
|
"flow_title": "{name} ({host})",
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"unsupported_device": "Only the primary powerview hub can be added",
|
"unsupported_device": "Only the primary PowerView Hub can be added",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"jvc_power": {
|
"jvc_power": {
|
||||||
"name": "[%key:component::sensor::entity_component::power::name%]"
|
"name": "[%key:component::binary_sensor::entity_component::power::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"request_data": {
|
"request_data": {
|
||||||
"name": "Request data",
|
"name": "Request data",
|
||||||
"description": "Requesta new data from the charging station."
|
"description": "Requests new data from the charging station."
|
||||||
},
|
},
|
||||||
"authorize": {
|
"authorize": {
|
||||||
"name": "Authorize",
|
"name": "Authorize",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"failsafe_timeout": {
|
"failsafe_timeout": {
|
||||||
"name": "Failsafe timeout",
|
"name": "Failsafe timeout",
|
||||||
"description": "Timeout after which the failsafe mode is triggered, if set_current was not executed during this time."
|
"description": "Timeout after which the failsafe mode is triggered if the 'Set current' action was not run during this time."
|
||||||
},
|
},
|
||||||
"failsafe_fallback": {
|
"failsafe_fallback": {
|
||||||
"name": "Failsafe fallback",
|
"name": "Failsafe fallback",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"failsafe_persist": {
|
"failsafe_persist": {
|
||||||
"name": "Failsafe persist",
|
"name": "Failsafe persist",
|
||||||
"description": "If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot."
|
"description": "If set to 0, the failsafe option will be disabled after a charging station reboot. If set to 1, the failsafe option will survive a reboot."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
|
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["lacrosse_view"],
|
"loggers": ["lacrosse_view"],
|
||||||
"requirements": ["lacrosse-view==1.0.4"]
|
"requirements": ["lacrosse-view==1.1.1"]
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ class LaCrosseSensorEntityDescription(SensorEntityDescription):
|
|||||||
|
|
||||||
def get_value(sensor: Sensor, field: str) -> float | int | str | None:
|
def get_value(sensor: Sensor, field: str) -> float | int | str | None:
|
||||||
"""Get the value of a sensor field."""
|
"""Get the value of a sensor field."""
|
||||||
field_data = sensor.data.get(field)
|
field_data = sensor.data.get(field) if sensor.data is not None else None
|
||||||
if field_data is None:
|
if field_data is None:
|
||||||
return None
|
return None
|
||||||
value = field_data["values"][-1]["s"]
|
value = field_data["values"][-1]["s"]
|
||||||
@ -178,7 +178,7 @@ async def async_setup_entry(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# if the API returns a different unit of measurement from the description, update it
|
# if the API returns a different unit of measurement from the description, update it
|
||||||
if sensor.data.get(field) is not None:
|
if sensor.data is not None and sensor.data.get(field) is not None:
|
||||||
native_unit_of_measurement = UNIT_OF_MEASUREMENT_MAP.get(
|
native_unit_of_measurement = UNIT_OF_MEASUREMENT_MAP.get(
|
||||||
sensor.data[field].get("unit")
|
sensor.data[field].get("unit")
|
||||||
)
|
)
|
||||||
@ -240,7 +240,9 @@ class LaCrosseViewSensor(
|
|||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
|
data = self.coordinator.data[self.index].data
|
||||||
return (
|
return (
|
||||||
super().available
|
super().available
|
||||||
and self.entity_description.key in self.coordinator.data[self.index].data
|
and data is not None
|
||||||
|
and self.entity_description.key in data
|
||||||
)
|
)
|
||||||
|
@ -7,8 +7,11 @@
|
|||||||
"express_mode": {
|
"express_mode": {
|
||||||
"default": "mdi:snowflake-variant"
|
"default": "mdi:snowflake-variant"
|
||||||
},
|
},
|
||||||
|
"express_fridge": {
|
||||||
|
"default": "mdi:snowflake"
|
||||||
|
},
|
||||||
"hot_water_mode": {
|
"hot_water_mode": {
|
||||||
"default": "mdi:list-status"
|
"default": "mdi:heat-wave"
|
||||||
},
|
},
|
||||||
"humidity_warm_mode": {
|
"humidity_warm_mode": {
|
||||||
"default": "mdi:heat-wave"
|
"default": "mdi:heat-wave"
|
||||||
@ -39,6 +42,9 @@
|
|||||||
},
|
},
|
||||||
"warm_mode": {
|
"warm_mode": {
|
||||||
"default": "mdi:heat-wave"
|
"default": "mdi:heat-wave"
|
||||||
|
},
|
||||||
|
"display_light": {
|
||||||
|
"default": "mdi:lightbulb-on-outline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["thinqconnect"],
|
"loggers": ["thinqconnect"],
|
||||||
"requirements": ["thinqconnect==1.0.2"]
|
"requirements": ["thinqconnect==1.0.4"]
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,13 @@
|
|||||||
"name": "Auto mode"
|
"name": "Auto mode"
|
||||||
},
|
},
|
||||||
"express_mode": {
|
"express_mode": {
|
||||||
"name": "Ice plus"
|
"name": "Express mode"
|
||||||
|
},
|
||||||
|
"express_fridge": {
|
||||||
|
"name": "Express cool"
|
||||||
},
|
},
|
||||||
"hot_water_mode": {
|
"hot_water_mode": {
|
||||||
"name": "Hot water"
|
"name": "Heating water"
|
||||||
},
|
},
|
||||||
"humidity_warm_mode": {
|
"humidity_warm_mode": {
|
||||||
"name": "Warm mist"
|
"name": "Warm mist"
|
||||||
@ -64,6 +67,9 @@
|
|||||||
},
|
},
|
||||||
"warm_mode": {
|
"warm_mode": {
|
||||||
"name": "Heating"
|
"name": "Heating"
|
||||||
|
},
|
||||||
|
"display_light": {
|
||||||
|
"name": "Lighting"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user