mirror of
https://github.com/home-assistant/core.git
synced 2026-09-03 18:24:49 -05:00
Merge branch 'dev' into programTemp
This commit is contained in:
@@ -36,7 +36,7 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 6
|
||||
HA_SHORT_VERSION: "2023.12"
|
||||
HA_SHORT_VERSION: "2024.1"
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
ALL_PYTHON_VERSIONS: "['3.11', '3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
@@ -120,6 +120,7 @@ homeassistant.components.energy.*
|
||||
homeassistant.components.esphome.*
|
||||
homeassistant.components.event.*
|
||||
homeassistant.components.evil_genius_labs.*
|
||||
homeassistant.components.faa_delays.*
|
||||
homeassistant.components.fan.*
|
||||
homeassistant.components.fastdotcom.*
|
||||
homeassistant.components.feedreader.*
|
||||
@@ -264,6 +265,7 @@ homeassistant.components.proximity.*
|
||||
homeassistant.components.prusalink.*
|
||||
homeassistant.components.pure_energie.*
|
||||
homeassistant.components.purpleair.*
|
||||
homeassistant.components.pushbullet.*
|
||||
homeassistant.components.pvoutput.*
|
||||
homeassistant.components.qnap_qsw.*
|
||||
homeassistant.components.radarr.*
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Automatically generated by hassfest.
|
||||
#
|
||||
# To update, run python3 -m script.hassfest -p docker
|
||||
ARG BUILD_FROM
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.aiohttp_compat import enable_compression
|
||||
from homeassistant.helpers.event import EventStateChangedData
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
@@ -218,9 +217,11 @@ class APIStatesView(HomeAssistantView):
|
||||
if entity_perm(state.entity_id, "read")
|
||||
)
|
||||
response = web.Response(
|
||||
body=f'[{",".join(states)}]', content_type=CONTENT_TYPE_JSON
|
||||
body=f'[{",".join(states)}]',
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
zlib_executor_size=32768,
|
||||
)
|
||||
enable_compression(response)
|
||||
response.enable_compression()
|
||||
return response
|
||||
|
||||
|
||||
@@ -390,17 +391,14 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
)
|
||||
|
||||
try:
|
||||
async with timeout(SERVICE_WAIT_TIMEOUT):
|
||||
# shield the service call from cancellation on connection drop
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain, service, data, blocking=True, context=context
|
||||
)
|
||||
# shield the service call from cancellation on connection drop
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain, service, data, blocking=True, context=context
|
||||
)
|
||||
)
|
||||
except (vol.Invalid, ServiceNotFound) as ex:
|
||||
raise HTTPBadRequest() from ex
|
||||
except TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
cancel_listen()
|
||||
|
||||
|
||||
@@ -1024,39 +1024,38 @@ class PipelineRun:
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_engine,
|
||||
language=self.pipeline.tts_language,
|
||||
options=self.tts_options,
|
||||
)
|
||||
tts_media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
tts_media_id,
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
if tts_input := tts_input.strip():
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_engine,
|
||||
language=self.pipeline.tts_language,
|
||||
options=self.tts_options,
|
||||
)
|
||||
tts_media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
tts_media_id,
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
tts_output = {
|
||||
"media_id": tts_media_id,
|
||||
**asdict(tts_media),
|
||||
}
|
||||
else:
|
||||
tts_output = {}
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.TTS_END,
|
||||
{
|
||||
"tts_output": {
|
||||
"media_id": tts_media_id,
|
||||
**asdict(tts_media),
|
||||
}
|
||||
},
|
||||
)
|
||||
PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output})
|
||||
)
|
||||
|
||||
return tts_media.url
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the device",
|
||||
"description": "Connect to the device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Atag device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
"flow_title": "{name} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up Axis device",
|
||||
"description": "Set up an Axis device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Axis device.",
|
||||
"username": "The user name you set up on your Axis device. It is recommended to create a user specifically for Home Assistant."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -93,8 +93,6 @@ class BAFFan(BAFEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode != PRESET_MODE_AUTO:
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
self._device.fan_mode = OffOnAuto.AUTO
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the Balboa Wi-Fi device",
|
||||
"description": "Connect to the Balboa Wi-Fi device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your Balboa Spa Wifi Device. For example, 192.168.1.58."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer-connected==0.14.3"]
|
||||
"requirements": ["bimmer-connected[china]==0.14.5"]
|
||||
}
|
||||
|
||||
@@ -199,10 +199,6 @@ class BondFan(BondEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action(
|
||||
Action.BREEZE_ON
|
||||
):
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your Bond hub."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"title": "SHC authentication parameters",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Bosch Smart Home Controller."
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
"flow_title": "{name} ({model} at {host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the device",
|
||||
"description": "Connect to the device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"timeout": "Timeout"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Broadlink device."
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
|
||||
@@ -60,8 +60,7 @@ async def async_setup_entry(
|
||||
data.static,
|
||||
entry,
|
||||
)
|
||||
],
|
||||
True,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"passkey": "Passkey string",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your BSB-Lan device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import DAVError, NotFoundError
|
||||
@@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .api import async_get_calendars, get_attr_value
|
||||
from .const import DOMAIN
|
||||
@@ -71,6 +72,12 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
||||
or (summary := get_attr_value(todo, "summary")) is None
|
||||
):
|
||||
return None
|
||||
due: date | datetime | None = None
|
||||
if due_value := get_attr_value(todo, "due"):
|
||||
if isinstance(due_value, datetime):
|
||||
due = dt_util.as_local(due_value)
|
||||
elif isinstance(due_value, date):
|
||||
due = due_value
|
||||
return TodoItem(
|
||||
uid=uid,
|
||||
summary=summary,
|
||||
@@ -78,9 +85,28 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
||||
get_attr_value(todo, "status") or "",
|
||||
TodoItemStatus.NEEDS_ACTION,
|
||||
),
|
||||
due=due,
|
||||
description=get_attr_value(todo, "description"),
|
||||
)
|
||||
|
||||
|
||||
def _to_ics_fields(item: TodoItem) -> dict[str, Any]:
|
||||
"""Convert a TodoItem to the set of add or update arguments."""
|
||||
item_data: dict[str, Any] = {}
|
||||
if summary := item.summary:
|
||||
item_data["summary"] = summary
|
||||
if status := item.status:
|
||||
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
||||
if due := item.due:
|
||||
if isinstance(due, datetime):
|
||||
item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ")
|
||||
else:
|
||||
item_data["due"] = due.strftime("%Y%m%d")
|
||||
if description := item.description:
|
||||
item_data["description"] = description
|
||||
return item_data
|
||||
|
||||
|
||||
class WebDavTodoListEntity(TodoListEntity):
|
||||
"""CalDAV To-do list entity."""
|
||||
|
||||
@@ -89,6 +115,9 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
|
||||
@@ -116,13 +145,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
"""Add an item to the To-do list."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
self._calendar.save_todo,
|
||||
summary=item.summary,
|
||||
status=TODO_STATUS_MAP_INV.get(
|
||||
item.status or TodoItemStatus.NEEDS_ACTION, "NEEDS-ACTION"
|
||||
),
|
||||
),
|
||||
partial(self._calendar.save_todo, **_to_ics_fields(item)),
|
||||
)
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
@@ -139,10 +162,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||
if item.summary:
|
||||
vtodo["summary"] = item.summary
|
||||
if item.status:
|
||||
vtodo["status"] = TODO_STATUS_MAP_INV.get(item.status, "NEEDS-ACTION")
|
||||
vtodo.update(**_to_ics_fields(item))
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
|
||||
@@ -68,13 +68,13 @@ class ComelitSerialBridge(DataUpdateCoordinator):
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update device data."""
|
||||
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
|
||||
|
||||
try:
|
||||
await self.api.login()
|
||||
return await self.api.get_all_devices()
|
||||
except exceptions.CannotConnect as err:
|
||||
_LOGGER.warning("Connection error for %s", self._host)
|
||||
await self.api.close()
|
||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
||||
except exceptions.CannotAuthenticate:
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
return await self.api.get_all_devices()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.5.2"]
|
||||
"requirements": ["aiocomelit==0.6.2"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Comelit device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.17"]
|
||||
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.29"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up your CoolMasterNet connection details.",
|
||||
"description": "Set up your CoolMasterNet connection details.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"off": "Can be turned off",
|
||||
@@ -12,6 +12,9 @@
|
||||
"dry": "Support dry mode",
|
||||
"fan_only": "Support fan only mode",
|
||||
"swing_support": "Control swing mode"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your CoolMasterNet device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -161,12 +161,9 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if self.preset_modes and preset_mode in self.preset_modes:
|
||||
self._preset_mode = preset_mode
|
||||
self._percentage = None
|
||||
self.schedule_update_ha_state()
|
||||
else:
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
self._preset_mode = preset_mode
|
||||
self._percentage = None
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(
|
||||
self,
|
||||
@@ -230,10 +227,6 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if self.preset_modes is None or preset_mode not in self.preset_modes:
|
||||
raise ValueError(
|
||||
f"{preset_mode} is not a valid preset_mode: {self.preset_modes}"
|
||||
)
|
||||
self._preset_mode = preset_mode
|
||||
self._percentage = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
class DevialetCoordinator(DataUpdateCoordinator):
|
||||
class DevialetCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Devialet update coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: DevialetApi) -> None:
|
||||
@@ -27,6 +27,6 @@ class DevialetCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self):
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
await self.client.async_update()
|
||||
|
||||
@@ -46,13 +46,15 @@ async def async_setup_entry(
|
||||
async_add_entities([DevialetMediaPlayerEntity(coordinator, entry)])
|
||||
|
||||
|
||||
class DevialetMediaPlayerEntity(CoordinatorEntity, MediaPlayerEntity):
|
||||
class DevialetMediaPlayerEntity(
|
||||
CoordinatorEntity[DevialetCoordinator], MediaPlayerEntity
|
||||
):
|
||||
"""Devialet media player."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator, entry: ConfigEntry) -> None:
|
||||
def __init__(self, coordinator: DevialetCoordinator, entry: ConfigEntry) -> None:
|
||||
"""Initialize the Devialet device."""
|
||||
self.coordinator = coordinator
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -29,6 +29,7 @@ DATA_TASK = "task"
|
||||
|
||||
DEVICE_NAME_ELECTRICITY = "Electricity Meter"
|
||||
DEVICE_NAME_GAS = "Gas Meter"
|
||||
DEVICE_NAME_WATER = "Water Meter"
|
||||
|
||||
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.const import (
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
@@ -57,6 +58,7 @@ from .const import (
|
||||
DEFAULT_TIME_BETWEEN_UPDATE,
|
||||
DEVICE_NAME_ELECTRICITY,
|
||||
DEVICE_NAME_GAS,
|
||||
DEVICE_NAME_WATER,
|
||||
DOMAIN,
|
||||
DSMR_PROTOCOL,
|
||||
LOGGER,
|
||||
@@ -73,6 +75,7 @@ class DSMRSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
dsmr_versions: set[str] | None = None
|
||||
is_gas: bool = False
|
||||
is_water: bool = False
|
||||
obis_reference: str
|
||||
|
||||
|
||||
@@ -374,28 +377,138 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
|
||||
"""Return correct entity for 5B Gas meter."""
|
||||
ref = None
|
||||
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS2_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS3_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS4_METER_READING2
|
||||
elif ref is None:
|
||||
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||
return DSMRSensorEntityDescription(
|
||||
key="belgium_5min_gas_meter_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
obis_reference=ref,
|
||||
dsmr_versions={"5B"},
|
||||
is_gas=True,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
def create_mbus_entity(
|
||||
mbus: int, mtype: int, telegram: dict[str, DSMRObject]
|
||||
) -> DSMRSensorEntityDescription | None:
|
||||
"""Create a new MBUS Entity."""
|
||||
if (
|
||||
mtype == 3
|
||||
and (
|
||||
obis_reference := getattr(
|
||||
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2"
|
||||
)
|
||||
)
|
||||
in telegram
|
||||
):
|
||||
return DSMRSensorEntityDescription(
|
||||
key=f"mbus{mbus}_gas_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
obis_reference=obis_reference,
|
||||
is_gas=True,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
if (
|
||||
mtype == 7
|
||||
and (
|
||||
obis_reference := getattr(
|
||||
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1"
|
||||
)
|
||||
)
|
||||
in telegram
|
||||
):
|
||||
return DSMRSensorEntityDescription(
|
||||
key=f"mbus{mbus}_water_reading",
|
||||
translation_key="water_meter_reading",
|
||||
obis_reference=obis_reference,
|
||||
is_water=True,
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def device_class_and_uom(
|
||||
telegram: dict[str, DSMRObject],
|
||||
entity_description: DSMRSensorEntityDescription,
|
||||
) -> tuple[SensorDeviceClass | None, str | None]:
|
||||
"""Get native unit of measurement from telegram,."""
|
||||
dsmr_object = telegram[entity_description.obis_reference]
|
||||
uom: str | None = getattr(dsmr_object, "unit") or None
|
||||
with suppress(ValueError):
|
||||
if entity_description.device_class == SensorDeviceClass.GAS and (
|
||||
enery_uom := UnitOfEnergy(str(uom))
|
||||
):
|
||||
return (SensorDeviceClass.ENERGY, enery_uom)
|
||||
if uom in UNIT_CONVERSION:
|
||||
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
||||
return (entity_description.device_class, uom)
|
||||
|
||||
|
||||
def rename_old_gas_to_mbus(
|
||||
hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str
|
||||
) -> None:
|
||||
"""Rename old gas sensor to mbus variant."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
||||
if device_entry_v1 is not None:
|
||||
device_id = device_entry_v1.id
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
entries = er.async_entries_for_device(ent_reg, device_id)
|
||||
|
||||
for entity in entries:
|
||||
if entity.unique_id.endswith("belgium_5min_gas_meter_reading"):
|
||||
try:
|
||||
ent_reg.async_update_entity(
|
||||
entity.entity_id,
|
||||
new_unique_id=mbus_device_id,
|
||||
device_id=mbus_device_id,
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.debug(
|
||||
"Skip migration of %s because it already exists",
|
||||
entity.entity_id,
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Migrated entity %s from unique id %s to %s",
|
||||
entity.entity_id,
|
||||
entity.unique_id,
|
||||
mbus_device_id,
|
||||
)
|
||||
# Cleanup old device
|
||||
dev_entities = er.async_entries_for_device(
|
||||
ent_reg, device_id, include_disabled_entities=True
|
||||
)
|
||||
if not dev_entities:
|
||||
dev_reg.async_remove_device(device_id)
|
||||
|
||||
|
||||
def create_mbus_entities(
|
||||
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
|
||||
) -> list[DSMREntity]:
|
||||
"""Create MBUS Entities."""
|
||||
entities = []
|
||||
for idx in range(1, 5):
|
||||
if (
|
||||
device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE")
|
||||
) not in telegram:
|
||||
continue
|
||||
if (type_ := int(telegram[device_type].value)) not in (3, 7):
|
||||
continue
|
||||
if (
|
||||
identifier := getattr(
|
||||
obis_references,
|
||||
f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER",
|
||||
)
|
||||
) in telegram:
|
||||
serial_ = telegram[identifier].value
|
||||
rename_old_gas_to_mbus(hass, entry, serial_)
|
||||
else:
|
||||
serial_ = ""
|
||||
if description := create_mbus_entity(idx, type_, telegram):
|
||||
entities.append(
|
||||
DSMREntity(
|
||||
description,
|
||||
entry,
|
||||
telegram,
|
||||
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
||||
serial_,
|
||||
idx,
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -415,25 +528,10 @@ async def async_setup_entry(
|
||||
add_entities_handler()
|
||||
add_entities_handler = None
|
||||
|
||||
def device_class_and_uom(
|
||||
telegram: dict[str, DSMRObject],
|
||||
entity_description: DSMRSensorEntityDescription,
|
||||
) -> tuple[SensorDeviceClass | None, str | None]:
|
||||
"""Get native unit of measurement from telegram,."""
|
||||
dsmr_object = telegram[entity_description.obis_reference]
|
||||
uom: str | None = getattr(dsmr_object, "unit") or None
|
||||
with suppress(ValueError):
|
||||
if entity_description.device_class == SensorDeviceClass.GAS and (
|
||||
enery_uom := UnitOfEnergy(str(uom))
|
||||
):
|
||||
return (SensorDeviceClass.ENERGY, enery_uom)
|
||||
if uom in UNIT_CONVERSION:
|
||||
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
||||
return (entity_description.device_class, uom)
|
||||
|
||||
all_sensors = SENSORS
|
||||
if dsmr_version == "5B":
|
||||
all_sensors += (add_gas_sensor_5B(telegram),)
|
||||
mbus_entities = create_mbus_entities(hass, telegram, entry)
|
||||
for mbus_entity in mbus_entities:
|
||||
entities.append(mbus_entity)
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
@@ -443,7 +541,7 @@ async def async_setup_entry(
|
||||
telegram,
|
||||
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
||||
)
|
||||
for description in all_sensors
|
||||
for description in SENSORS
|
||||
if (
|
||||
description.dsmr_versions is None
|
||||
or dsmr_version in description.dsmr_versions
|
||||
@@ -618,6 +716,8 @@ class DSMREntity(SensorEntity):
|
||||
telegram: dict[str, DSMRObject],
|
||||
device_class: SensorDeviceClass,
|
||||
native_unit_of_measurement: str | None,
|
||||
serial_id: str = "",
|
||||
mbus_id: int = 0,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
self.entity_description = entity_description
|
||||
@@ -629,8 +729,15 @@ class DSMREntity(SensorEntity):
|
||||
device_serial = entry.data[CONF_SERIAL_ID]
|
||||
device_name = DEVICE_NAME_ELECTRICITY
|
||||
if entity_description.is_gas:
|
||||
device_serial = entry.data[CONF_SERIAL_ID_GAS]
|
||||
if serial_id:
|
||||
device_serial = serial_id
|
||||
else:
|
||||
device_serial = entry.data[CONF_SERIAL_ID_GAS]
|
||||
device_name = DEVICE_NAME_GAS
|
||||
if entity_description.is_water:
|
||||
if serial_id:
|
||||
device_serial = serial_id
|
||||
device_name = DEVICE_NAME_WATER
|
||||
if device_serial is None:
|
||||
device_serial = entry.entry_id
|
||||
|
||||
@@ -638,7 +745,13 @@ class DSMREntity(SensorEntity):
|
||||
identifiers={(DOMAIN, device_serial)},
|
||||
name=device_name,
|
||||
)
|
||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||
if mbus_id != 0:
|
||||
if serial_id:
|
||||
self._attr_unique_id = f"{device_serial}"
|
||||
else:
|
||||
self._attr_unique_id = f"{device_serial}_{mbus_id}"
|
||||
else:
|
||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||
|
||||
@callback
|
||||
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
|
||||
|
||||
@@ -147,6 +147,9 @@
|
||||
},
|
||||
"voltage_swell_l3_count": {
|
||||
"name": "Voltage swells phase L3"
|
||||
},
|
||||
"water_meter_reading": {
|
||||
"name": "Water consumption"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantAccessLogger
|
||||
from homeassistant.components.network import async_get_source_ip
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITIES,
|
||||
@@ -101,7 +100,7 @@ async def start_emulated_hue_bridge(
|
||||
config.advertise_port or config.listen_port,
|
||||
)
|
||||
|
||||
runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger)
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
|
||||
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
|
||||
|
||||
@@ -173,8 +173,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if self._static_info.supports_target_humidity:
|
||||
features |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if self._static_info.supports_aux_heat:
|
||||
features |= ClimateEntityFeature.AUX_HEAT
|
||||
if self.preset_modes:
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
if self.fan_modes:
|
||||
@@ -272,12 +270,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
"""Return the humidity we try to reach."""
|
||||
return round(self._state.target_humidity)
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Return the auxiliary heater state."""
|
||||
return self._state.aux_heat
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature (and operation mode if set)."""
|
||||
data: dict[str, Any] = {"key": self._key}
|
||||
@@ -326,11 +318,3 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
await self._client.climate_command(
|
||||
key=self._key, swing_mode=_SWING_MODES.from_hass(swing_mode)
|
||||
)
|
||||
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
await self._client.climate_command(key=self._key, aux_heat=True)
|
||||
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
await self._client.climate_command(key=self._key, aux_heat=False)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==19.2.0",
|
||||
"aioesphomeapi==19.2.1",
|
||||
"bluetooth-data-tools==1.15.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
|
||||
@@ -186,16 +186,22 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
data_to_send = {"text": event.data["tts_input"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END:
|
||||
assert event.data is not None
|
||||
path = event.data["tts_output"]["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
tts_output = event.data["tts_output"]
|
||||
if tts_output:
|
||||
path = tts_output["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
|
||||
if self.device_info.voice_assistant_version >= 2:
|
||||
media_id = event.data["tts_output"]["media_id"]
|
||||
self._tts_task = self.hass.async_create_background_task(
|
||||
self._send_tts(media_id), "esphome_voice_assistant_tts"
|
||||
)
|
||||
if self.device_info.voice_assistant_version >= 2:
|
||||
media_id = tts_output["media_id"]
|
||||
self._tts_task = self.hass.async_create_background_task(
|
||||
self._send_tts(media_id), "esphome_voice_assistant_tts"
|
||||
)
|
||||
else:
|
||||
self._tts_done.set()
|
||||
else:
|
||||
# Empty TTS response
|
||||
data_to_send = {}
|
||||
self._tts_done.set()
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END:
|
||||
assert event.data is not None
|
||||
|
||||
@@ -1,44 +1,88 @@
|
||||
"""Platform for FAA Delays sensor component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from faadelays import Airport
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import FAADataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
FAA_BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
|
||||
BinarySensorEntityDescription(
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class FaaDelaysBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Mixin for required keys."""
|
||||
|
||||
is_on_fn: Callable[[Airport], bool | None]
|
||||
extra_state_attributes_fn: Callable[[Airport], Mapping[str, Any]]
|
||||
|
||||
|
||||
FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = (
|
||||
FaaDelaysBinarySensorEntityDescription(
|
||||
key="GROUND_DELAY",
|
||||
name="Ground Delay",
|
||||
translation_key="ground_delay",
|
||||
icon="mdi:airport",
|
||||
is_on_fn=lambda airport: airport.ground_delay.status,
|
||||
extra_state_attributes_fn=lambda airport: {
|
||||
"average": airport.ground_delay.average,
|
||||
"reason": airport.ground_delay.reason,
|
||||
},
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
FaaDelaysBinarySensorEntityDescription(
|
||||
key="GROUND_STOP",
|
||||
name="Ground Stop",
|
||||
translation_key="ground_stop",
|
||||
icon="mdi:airport",
|
||||
is_on_fn=lambda airport: airport.ground_stop.status,
|
||||
extra_state_attributes_fn=lambda airport: {
|
||||
"endtime": airport.ground_stop.endtime,
|
||||
"reason": airport.ground_stop.reason,
|
||||
},
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
FaaDelaysBinarySensorEntityDescription(
|
||||
key="DEPART_DELAY",
|
||||
name="Departure Delay",
|
||||
translation_key="depart_delay",
|
||||
icon="mdi:airplane-takeoff",
|
||||
is_on_fn=lambda airport: airport.depart_delay.status,
|
||||
extra_state_attributes_fn=lambda airport: {
|
||||
"minimum": airport.depart_delay.minimum,
|
||||
"maximum": airport.depart_delay.maximum,
|
||||
"trend": airport.depart_delay.trend,
|
||||
"reason": airport.depart_delay.reason,
|
||||
},
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
FaaDelaysBinarySensorEntityDescription(
|
||||
key="ARRIVE_DELAY",
|
||||
name="Arrival Delay",
|
||||
translation_key="arrive_delay",
|
||||
icon="mdi:airplane-landing",
|
||||
is_on_fn=lambda airport: airport.arrive_delay.status,
|
||||
extra_state_attributes_fn=lambda airport: {
|
||||
"minimum": airport.arrive_delay.minimum,
|
||||
"maximum": airport.arrive_delay.maximum,
|
||||
"trend": airport.arrive_delay.trend,
|
||||
"reason": airport.arrive_delay.reason,
|
||||
},
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
FaaDelaysBinarySensorEntityDescription(
|
||||
key="CLOSURE",
|
||||
name="Closure",
|
||||
translation_key="closure",
|
||||
icon="mdi:airplane:off",
|
||||
is_on_fn=lambda airport: airport.closure.status,
|
||||
extra_state_attributes_fn=lambda airport: {
|
||||
"begin": airport.closure.start,
|
||||
"end": airport.closure.end,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -57,60 +101,38 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class FAABinarySensor(CoordinatorEntity, BinarySensorEntity):
|
||||
class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorEntity):
|
||||
"""Define a binary sensor for FAA Delays."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
entity_description: FaaDelaysBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, coordinator, entry_id, description: BinarySensorEntityDescription
|
||||
self,
|
||||
coordinator: FAADataUpdateCoordinator,
|
||||
entry_id: str,
|
||||
description: FaaDelaysBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
|
||||
self.coordinator = coordinator
|
||||
self._entry_id = entry_id
|
||||
self._attrs: dict[str, Any] = {}
|
||||
_id = coordinator.data.code
|
||||
self._attr_name = f"{_id} {description.name}"
|
||||
self._attr_unique_id = f"{_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, _id)},
|
||||
name=_id,
|
||||
manufacturer="Federal Aviation Administration",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the status of the sensor."""
|
||||
sensor_type = self.entity_description.key
|
||||
if sensor_type == "GROUND_DELAY":
|
||||
return self.coordinator.data.ground_delay.status
|
||||
if sensor_type == "GROUND_STOP":
|
||||
return self.coordinator.data.ground_stop.status
|
||||
if sensor_type == "DEPART_DELAY":
|
||||
return self.coordinator.data.depart_delay.status
|
||||
if sensor_type == "ARRIVE_DELAY":
|
||||
return self.coordinator.data.arrive_delay.status
|
||||
if sensor_type == "CLOSURE":
|
||||
return self.coordinator.data.closure.status
|
||||
return None
|
||||
return self.entity_description.is_on_fn(self.coordinator.data)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> Mapping[str, Any]:
|
||||
"""Return attributes for sensor."""
|
||||
sensor_type = self.entity_description.key
|
||||
if sensor_type == "GROUND_DELAY":
|
||||
self._attrs["average"] = self.coordinator.data.ground_delay.average
|
||||
self._attrs["reason"] = self.coordinator.data.ground_delay.reason
|
||||
elif sensor_type == "GROUND_STOP":
|
||||
self._attrs["endtime"] = self.coordinator.data.ground_stop.endtime
|
||||
self._attrs["reason"] = self.coordinator.data.ground_stop.reason
|
||||
elif sensor_type == "DEPART_DELAY":
|
||||
self._attrs["minimum"] = self.coordinator.data.depart_delay.minimum
|
||||
self._attrs["maximum"] = self.coordinator.data.depart_delay.maximum
|
||||
self._attrs["trend"] = self.coordinator.data.depart_delay.trend
|
||||
self._attrs["reason"] = self.coordinator.data.depart_delay.reason
|
||||
elif sensor_type == "ARRIVE_DELAY":
|
||||
self._attrs["minimum"] = self.coordinator.data.arrive_delay.minimum
|
||||
self._attrs["maximum"] = self.coordinator.data.arrive_delay.maximum
|
||||
self._attrs["trend"] = self.coordinator.data.arrive_delay.trend
|
||||
self._attrs["reason"] = self.coordinator.data.arrive_delay.reason
|
||||
elif sensor_type == "CLOSURE":
|
||||
self._attrs["begin"] = self.coordinator.data.closure.start
|
||||
self._attrs["end"] = self.coordinator.data.closure.end
|
||||
return self._attrs
|
||||
return self.entity_description.extra_state_attributes_fn(self.coordinator.data)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Config flow for FAA Delays integration."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
import faadelays
|
||||
@@ -7,6 +8,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -21,7 +23,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from aiohttp import ClientConnectionError
|
||||
from faadelays import Airport
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -14,19 +15,18 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FAADataUpdateCoordinator(DataUpdateCoordinator):
|
||||
class FAADataUpdateCoordinator(DataUpdateCoordinator[Airport]):
|
||||
"""Class to manage fetching FAA API data from a single endpoint."""
|
||||
|
||||
def __init__(self, hass, code):
|
||||
def __init__(self, hass: HomeAssistant, code: str) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=1)
|
||||
)
|
||||
self.session = aiohttp_client.async_get_clientsession(hass)
|
||||
self.data = Airport(code, self.session)
|
||||
self.code = code
|
||||
|
||||
async def _async_update_data(self):
|
||||
async def _async_update_data(self) -> Airport:
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await self.data.update()
|
||||
|
||||
@@ -17,5 +17,76 @@
|
||||
"abort": {
|
||||
"already_configured": "This airport is already configured."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"ground_delay": {
|
||||
"name": "Ground delay",
|
||||
"state_attributes": {
|
||||
"average": {
|
||||
"name": "Average"
|
||||
},
|
||||
"reason": {
|
||||
"name": "Reason"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ground_stop": {
|
||||
"name": "Ground stop",
|
||||
"state_attributes": {
|
||||
"endtime": {
|
||||
"name": "End time"
|
||||
},
|
||||
"reason": {
|
||||
"name": "[%key:component::faa_delays::entity::binary_sensor::ground_delay::state_attributes::reason::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"depart_delay": {
|
||||
"name": "Departure delay",
|
||||
"state_attributes": {
|
||||
"minimum": {
|
||||
"name": "Minimum"
|
||||
},
|
||||
"maximum": {
|
||||
"name": "Maximum"
|
||||
},
|
||||
"trend": {
|
||||
"name": "Trend"
|
||||
},
|
||||
"reason": {
|
||||
"name": "[%key:component::faa_delays::entity::binary_sensor::ground_delay::state_attributes::reason::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"arrive_delay": {
|
||||
"name": "Arrival delay",
|
||||
"state_attributes": {
|
||||
"minimum": {
|
||||
"name": "[%key:component::faa_delays::entity::binary_sensor::depart_delay::state_attributes::minimum::name%]"
|
||||
},
|
||||
"maximum": {
|
||||
"name": "[%key:component::faa_delays::entity::binary_sensor::depart_delay::state_attributes::maximum::name%]"
|
||||
},
|
||||
"trend": {
|
||||
"name": "[%key:component::faa_delays::entity::binary_sensor::depart_delay::state_attributes::trend::name%]"
|
||||
},
|
||||
"reason": {
|
||||
"name": "[%key:component::faa_delays::entity::binary_sensor::ground_delay::state_attributes::reason::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"closure": {
|
||||
"name": "Closure",
|
||||
"state_attributes": {
|
||||
"begin": {
|
||||
"name": "Begin"
|
||||
},
|
||||
"end": {
|
||||
"name": "End"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
@@ -77,8 +78,19 @@ ATTR_PRESET_MODES = "preset_modes"
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
class NotValidPresetModeError(ValueError):
|
||||
"""Exception class when the preset_mode in not in the preset_modes list."""
|
||||
class NotValidPresetModeError(ServiceValidationError):
|
||||
"""Raised when the preset_mode is not in the preset_modes list."""
|
||||
|
||||
def __init__(
|
||||
self, *args: object, translation_placeholders: dict[str, str] | None = None
|
||||
) -> None:
|
||||
"""Initialize the exception."""
|
||||
super().__init__(
|
||||
*args,
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_valid_preset_mode",
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@@ -107,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
),
|
||||
vol.Optional(ATTR_PRESET_MODE): cv.string,
|
||||
},
|
||||
"async_turn_on",
|
||||
"async_handle_turn_on_service",
|
||||
)
|
||||
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
|
||||
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
||||
@@ -156,7 +168,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{vol.Required(ATTR_PRESET_MODE): cv.string},
|
||||
"async_set_preset_mode",
|
||||
"async_handle_set_preset_mode_service",
|
||||
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
|
||||
)
|
||||
|
||||
@@ -237,17 +249,30 @@ class FanEntity(ToggleEntity):
|
||||
"""Set new preset mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@final
|
||||
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
|
||||
"""Validate and set new preset mode."""
|
||||
self._valid_preset_mode_or_raise(preset_mode)
|
||||
await self.async_set_preset_mode(preset_mode)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
|
||||
|
||||
@final
|
||||
@callback
|
||||
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
|
||||
"""Raise NotValidPresetModeError on invalid preset_mode."""
|
||||
preset_modes = self.preset_modes
|
||||
if not preset_modes or preset_mode not in preset_modes:
|
||||
preset_modes_str: str = ", ".join(preset_modes or [])
|
||||
raise NotValidPresetModeError(
|
||||
f"The preset_mode {preset_mode} is not a valid preset_mode:"
|
||||
f" {preset_modes}"
|
||||
f" {preset_modes}",
|
||||
translation_placeholders={
|
||||
"preset_mode": preset_mode,
|
||||
"preset_modes": preset_modes_str,
|
||||
},
|
||||
)
|
||||
|
||||
def set_direction(self, direction: str) -> None:
|
||||
@@ -267,6 +292,18 @@ class FanEntity(ToggleEntity):
|
||||
"""Turn on the fan."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@final
|
||||
async def async_handle_turn_on_service(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Validate and turn on the fan."""
|
||||
if preset_mode is not None:
|
||||
self._valid_preset_mode_or_raise(preset_mode)
|
||||
await self.async_turn_on(percentage, preset_mode, **kwargs)
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
|
||||
@@ -144,5 +144,10 @@
|
||||
"reverse": "Reverse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"not_valid_preset_mode": {
|
||||
"message": "Preset mode {preset_mode} is not valid, valid preset modes are: {preset_modes}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +131,9 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if command := PRESET_TO_COMMAND.get(preset_mode):
|
||||
async with self.coordinator.async_connect_and_update() as device:
|
||||
await device.send_command(command)
|
||||
else:
|
||||
raise UnsupportedPreset(f"The preset {preset_mode} is unsupported")
|
||||
command = PRESET_TO_COMMAND[preset_mode]
|
||||
async with self.coordinator.async_connect_and_update() as device:
|
||||
await device.send_command(command)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20231030.2"]
|
||||
"requirements": ["home-assistant-frontend==20231129.1"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -44,12 +45,14 @@ class GdacsSensor(SensorEntity):
|
||||
_attr_should_poll = False
|
||||
_attr_icon = DEFAULT_ICON
|
||||
_attr_native_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry, manager) -> None:
|
||||
"""Initialize entity."""
|
||||
assert config_entry.unique_id
|
||||
self._config_entry_id = config_entry.entry_id
|
||||
self._attr_unique_id = config_entry.unique_id
|
||||
self._attr_name = f"GDACS ({config_entry.title})"
|
||||
self._manager = manager
|
||||
self._status = None
|
||||
self._last_update = None
|
||||
@@ -60,6 +63,11 @@ class GdacsSensor(SensorEntity):
|
||||
self._updated = None
|
||||
self._removed = None
|
||||
self._remove_signal_status: Callable[[], None] | None = None
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.unique_id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer="GDACS",
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Google Tasks todo platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DOMAIN
|
||||
@@ -35,9 +36,31 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
|
||||
result["title"] = item.summary
|
||||
if item.status is not None:
|
||||
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
||||
if (due := item.due) is not None:
|
||||
# due API field is a timestamp string, but with only date resolution
|
||||
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
||||
if (description := item.description) is not None:
|
||||
result["notes"] = description
|
||||
return result
|
||||
|
||||
|
||||
def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
||||
"""Convert tasks API items into a TodoItem."""
|
||||
due: date | None = None
|
||||
if (due_str := item.get("due")) is not None:
|
||||
due = datetime.fromisoformat(due_str).date()
|
||||
return TodoItem(
|
||||
summary=item["title"],
|
||||
uid=item["id"],
|
||||
status=TODO_STATUS_MAP.get(
|
||||
item.get("status", ""),
|
||||
TodoItemStatus.NEEDS_ACTION,
|
||||
),
|
||||
due=due,
|
||||
description=item.get("notes"),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
@@ -68,6 +91,8 @@ class GoogleTaskTodoListEntity(
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -88,17 +113,7 @@ class GoogleTaskTodoListEntity(
|
||||
"""Get the current set of To-do items."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return [
|
||||
TodoItem(
|
||||
summary=item["title"],
|
||||
uid=item["id"],
|
||||
status=TODO_STATUS_MAP.get(
|
||||
item.get("status"), # type: ignore[arg-type]
|
||||
TodoItemStatus.NEEDS_ACTION,
|
||||
),
|
||||
)
|
||||
for item in _order_tasks(self.coordinator.data)
|
||||
]
|
||||
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
|
||||
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from .bridge import DiscoveryService
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DATA_DISCOVERY_INTERVAL,
|
||||
DATA_DISCOVERY_SERVICE,
|
||||
DISCOVERY_SCAN_INTERVAL,
|
||||
DISPATCHERS,
|
||||
@@ -29,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
gree_discovery = DiscoveryService(hass)
|
||||
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
|
||||
|
||||
hass.data[DOMAIN].setdefault(DISPATCHERS, [])
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def _async_scan_update(_=None):
|
||||
@@ -39,8 +37,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.debug("Scanning network for Gree devices")
|
||||
await _async_scan_update()
|
||||
|
||||
hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval(
|
||||
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval(
|
||||
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -48,13 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if hass.data[DOMAIN].get(DISPATCHERS) is not None:
|
||||
for cleanup in hass.data[DOMAIN][DISPATCHERS]:
|
||||
cleanup()
|
||||
|
||||
if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None:
|
||||
hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)()
|
||||
|
||||
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
|
||||
hass.data.pop(DATA_DISCOVERY_SERVICE)
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ from .bridge import DeviceDataUpdateCoordinator
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DISPATCH_DEVICE_DISCOVERED,
|
||||
DISPATCHERS,
|
||||
DOMAIN,
|
||||
FAN_MEDIUM_HIGH,
|
||||
FAN_MEDIUM_LOW,
|
||||
@@ -88,7 +87,7 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Gree HVAC device from a config entry."""
|
||||
@@ -101,7 +100,7 @@ async def async_setup_entry(
|
||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||
init_device(coordinator)
|
||||
|
||||
hass.data[DOMAIN][DISPATCHERS].append(
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
COORDINATORS = "coordinators"
|
||||
|
||||
DATA_DISCOVERY_SERVICE = "gree_discovery"
|
||||
DATA_DISCOVERY_INTERVAL = "gree_discovery_interval"
|
||||
|
||||
DISCOVERY_SCAN_INTERVAL = 300
|
||||
DISCOVERY_TIMEOUT = 8
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN
|
||||
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
|
||||
from .entity import GreeEntity
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Gree HVAC device from a config entry."""
|
||||
@@ -119,7 +119,7 @@ async def async_setup_entry(
|
||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||
init_device(coordinator)
|
||||
|
||||
hass.data[DOMAIN][DISPATCHERS].append(
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from http import HTTPStatus
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
import aiohttp
|
||||
@@ -156,6 +157,9 @@ class HassIOView(HomeAssistantView):
|
||||
# _stored_content_type is only computed once `content_type` is accessed
|
||||
if path == "backups/new/upload":
|
||||
# We need to reuse the full content type that includes the boundary
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable-next=protected-access
|
||||
assert isinstance(request._stored_content_type, str)
|
||||
# pylint: disable-next=protected-access
|
||||
headers[CONTENT_TYPE] = request._stored_content_type
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from yarl import URL
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_compat import enable_compression
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
|
||||
from .const import X_HASS_SOURCE, X_INGRESS_PATH
|
||||
@@ -172,7 +171,7 @@ class HassIOIngress(HomeAssistantView):
|
||||
content_length = result.headers.get(hdrs.CONTENT_LENGTH, UNDEFINED)
|
||||
# Avoid parsing content_type in simple cases for better performance
|
||||
if maybe_content_type := result.headers.get(hdrs.CONTENT_TYPE):
|
||||
content_type = (maybe_content_type.partition(";"))[0].strip()
|
||||
content_type: str = (maybe_content_type.partition(";"))[0].strip()
|
||||
else:
|
||||
content_type = result.content_type
|
||||
# Simple request
|
||||
@@ -188,11 +187,12 @@ class HassIOIngress(HomeAssistantView):
|
||||
status=result.status,
|
||||
content_type=content_type,
|
||||
body=body,
|
||||
zlib_executor_size=32768,
|
||||
)
|
||||
if content_length_int > MIN_COMPRESSED_SIZE and should_compress(
|
||||
content_type or simple_response.content_type
|
||||
):
|
||||
enable_compression(simple_response)
|
||||
simple_response.enable_compression()
|
||||
await simple_response.prepare(request)
|
||||
return simple_response
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import websocket_api
|
||||
from .const import DOMAIN
|
||||
from .helpers import entities_may_have_state_changes_after
|
||||
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
|
||||
|
||||
CONF_ORDER = "use_include_order"
|
||||
|
||||
@@ -106,7 +106,8 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
no_attributes = "no_attributes" in request.query
|
||||
|
||||
if (
|
||||
not include_start_time_state
|
||||
(end_time and not has_recorder_run_after(hass, end_time))
|
||||
or not include_start_time_state
|
||||
and entity_ids
|
||||
and not entities_may_have_state_changes_after(
|
||||
hass, entity_ids, start_time, no_attributes
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime as dt
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import process_timestamp
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@@ -21,3 +23,10 @@ def entities_may_have_state_changes_after(
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool:
|
||||
"""Check if the recorder has any runs after a specific time."""
|
||||
return run_time >= process_timestamp(
|
||||
get_instance(hass).recorder_runs_manager.first.start
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ from homeassistant.helpers.typing import EventType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
|
||||
from .helpers import entities_may_have_state_changes_after
|
||||
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -142,7 +142,8 @@ async def ws_get_history_during_period(
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
||||
if (
|
||||
not include_start_time_state
|
||||
(end_time and not has_recorder_run_after(hass, end_time))
|
||||
or not include_start_time_state
|
||||
and entity_ids
|
||||
and not entities_may_have_state_changes_after(
|
||||
hass, entity_ids, start_time, no_attributes
|
||||
|
||||
@@ -16,7 +16,6 @@ from aiohttp.http_parser import RawRequestMessage
|
||||
from aiohttp.streams import StreamReader
|
||||
from aiohttp.typedefs import JSONDecoder, StrOrURL
|
||||
from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection
|
||||
from aiohttp.web_log import AccessLogger
|
||||
from aiohttp.web_protocol import RequestHandler
|
||||
from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher
|
||||
from aiohttp_zlib_ng import enable_zlib_ng
|
||||
@@ -238,25 +237,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class HomeAssistantAccessLogger(AccessLogger):
|
||||
"""Access logger for Home Assistant that does not log when disabled."""
|
||||
|
||||
def log(
|
||||
self, request: web.BaseRequest, response: web.StreamResponse, time: float
|
||||
) -> None:
|
||||
"""Log the request.
|
||||
|
||||
The default implementation logs the request to the logger
|
||||
with the INFO level and than throws it away if the logger
|
||||
is not enabled for the INFO level. This implementation
|
||||
does not log the request if the logger is not enabled for
|
||||
the INFO level.
|
||||
"""
|
||||
if not self.logger.isEnabledFor(logging.INFO):
|
||||
return
|
||||
super().log(request, response, time)
|
||||
|
||||
|
||||
class HomeAssistantRequest(web.Request):
|
||||
"""Home Assistant request object."""
|
||||
|
||||
@@ -540,9 +520,7 @@ class HomeAssistantHTTP:
|
||||
# pylint: disable-next=protected-access
|
||||
self.app._router.freeze = lambda: None # type: ignore[method-assign]
|
||||
|
||||
self.runner = web.AppRunner(
|
||||
self.app, access_log_class=HomeAssistantAccessLogger
|
||||
)
|
||||
self.runner = web.AppRunner(self.app, handler_cancellation=True)
|
||||
await self.runner.setup()
|
||||
|
||||
self.site = HomeAssistantTCPSite(
|
||||
|
||||
@@ -20,7 +20,6 @@ import voluptuous as vol
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
from homeassistant.core import Context, HomeAssistant, is_callback
|
||||
from homeassistant.helpers.aiohttp_compat import enable_compression
|
||||
from homeassistant.helpers.json import (
|
||||
find_paths_unserializable_data,
|
||||
json_bytes,
|
||||
@@ -72,8 +71,9 @@ class HomeAssistantView:
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
status=int(status_code),
|
||||
headers=headers,
|
||||
zlib_executor_size=32768,
|
||||
)
|
||||
enable_compression(response)
|
||||
response.enable_compression()
|
||||
return response
|
||||
|
||||
def json_message(
|
||||
|
||||
@@ -8,8 +8,6 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
|
||||
from huawei_lte_api.enums.net import NetworkModeEnum
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorDeviceClass,
|
||||
@@ -575,10 +573,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"State": HuaweiSensorEntityDescription(
|
||||
key="State",
|
||||
translation_key="operator_search_mode",
|
||||
format_fn=lambda x: (
|
||||
{"0": "Auto", "1": "Manual"}.get(x),
|
||||
None,
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
},
|
||||
@@ -588,19 +582,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
descriptions={
|
||||
"NetworkMode": HuaweiSensorEntityDescription(
|
||||
key="NetworkMode",
|
||||
translation_key="preferred_mode",
|
||||
format_fn=lambda x: (
|
||||
{
|
||||
NetworkModeEnum.MODE_AUTO.value: "4G/3G/2G",
|
||||
NetworkModeEnum.MODE_4G_3G_AUTO.value: "4G/3G",
|
||||
NetworkModeEnum.MODE_4G_2G_AUTO.value: "4G/2G",
|
||||
NetworkModeEnum.MODE_4G_ONLY.value: "4G",
|
||||
NetworkModeEnum.MODE_3G_2G_AUTO.value: "3G/2G",
|
||||
NetworkModeEnum.MODE_3G_ONLY.value: "3G",
|
||||
NetworkModeEnum.MODE_2G_ONLY.value: "2G",
|
||||
}.get(x),
|
||||
None,
|
||||
),
|
||||
translation_key="preferred_network_mode",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
},
|
||||
|
||||
@@ -231,10 +231,23 @@
|
||||
"name": "Operator code"
|
||||
},
|
||||
"operator_search_mode": {
|
||||
"name": "Operator search mode"
|
||||
"name": "Operator search mode",
|
||||
"state": {
|
||||
"0": "Auto",
|
||||
"1": "Manual"
|
||||
}
|
||||
},
|
||||
"preferred_mode": {
|
||||
"name": "Preferred mode"
|
||||
"preferred_network_mode": {
|
||||
"name": "Preferred network mode",
|
||||
"state": {
|
||||
"00": "4G/3G/2G auto",
|
||||
"0302": "4G/3G auto",
|
||||
"0301": "4G/2G auto",
|
||||
"03": "4G only",
|
||||
"0201": "3G/2G auto",
|
||||
"02": "3G only",
|
||||
"01": "2G only"
|
||||
}
|
||||
},
|
||||
"sms_deleted_device": {
|
||||
"name": "SMS deleted (device)"
|
||||
|
||||
@@ -3,13 +3,18 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, TypeVar, cast
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from pykoplenti import ApiClient, ApiException, AuthenticationException
|
||||
from pykoplenti import (
|
||||
ApiClient,
|
||||
ApiException,
|
||||
AuthenticationException,
|
||||
ExtendedApiClient,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
@@ -51,7 +56,9 @@ class Plenticore:
|
||||
|
||||
async def async_setup(self) -> bool:
|
||||
"""Set up Plenticore API client."""
|
||||
self._client = ApiClient(async_get_clientsession(self.hass), host=self.host)
|
||||
self._client = ExtendedApiClient(
|
||||
async_get_clientsession(self.hass), host=self.host
|
||||
)
|
||||
try:
|
||||
await self._client.login(self.config_entry.data[CONF_PASSWORD])
|
||||
except AuthenticationException as err:
|
||||
@@ -124,7 +131,7 @@ class DataUpdateCoordinatorMixin:
|
||||
|
||||
async def async_read_data(
|
||||
self, module_id: str, data_id: str
|
||||
) -> dict[str, dict[str, str]] | None:
|
||||
) -> Mapping[str, Mapping[str, str]] | None:
|
||||
"""Read data from Plenticore."""
|
||||
if (client := self._plenticore.client) is None:
|
||||
return None
|
||||
@@ -190,7 +197,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator[_DataT]):
|
||||
|
||||
|
||||
class ProcessDataUpdateCoordinator(
|
||||
PlenticoreUpdateCoordinator[dict[str, dict[str, str]]]
|
||||
PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]]
|
||||
):
|
||||
"""Implementation of PlenticoreUpdateCoordinator for process data."""
|
||||
|
||||
@@ -206,18 +213,19 @@ class ProcessDataUpdateCoordinator(
|
||||
return {
|
||||
module_id: {
|
||||
process_data.id: process_data.value
|
||||
for process_data in fetched_data[module_id]
|
||||
for process_data in fetched_data[module_id].values()
|
||||
}
|
||||
for module_id in fetched_data
|
||||
}
|
||||
|
||||
|
||||
class SettingDataUpdateCoordinator(
|
||||
PlenticoreUpdateCoordinator[dict[str, dict[str, str]]], DataUpdateCoordinatorMixin
|
||||
PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]],
|
||||
DataUpdateCoordinatorMixin,
|
||||
):
|
||||
"""Implementation of PlenticoreUpdateCoordinator for settings data."""
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, str]]:
|
||||
async def _async_update_data(self) -> Mapping[str, Mapping[str, str]]:
|
||||
client = self._plenticore.client
|
||||
|
||||
if not self._fetch or client is None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/kostal_plenticore",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kostal"],
|
||||
"requirements": ["pykoplenti==1.0.0"]
|
||||
"requirements": ["pykoplenti==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -649,6 +649,39 @@ SENSOR_PROCESS_DATA = [
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="scb:statistic:EnergyFlow",
|
||||
key="Statistic:EnergyDischarge:Day",
|
||||
name="Battery Discharge Day",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="scb:statistic:EnergyFlow",
|
||||
key="Statistic:EnergyDischarge:Month",
|
||||
name="Battery Discharge Month",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="scb:statistic:EnergyFlow",
|
||||
key="Statistic:EnergyDischarge:Year",
|
||||
name="Battery Discharge Year",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="scb:statistic:EnergyFlow",
|
||||
key="Statistic:EnergyDischarge:Total",
|
||||
name="Battery Discharge Total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="scb:statistic:EnergyFlow",
|
||||
key="Statistic:EnergyDischargeGrid:Day",
|
||||
@@ -682,6 +715,52 @@ SENSOR_PROCESS_DATA = [
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="_virt_",
|
||||
key="pv_P",
|
||||
name="Sum power of all PV DC inputs",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
formatter="format_round",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="_virt_",
|
||||
key="Statistic:EnergyGrid:Total",
|
||||
name="Energy to Grid Total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="_virt_",
|
||||
key="Statistic:EnergyGrid:Year",
|
||||
name="Energy to Grid Year",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="_virt_",
|
||||
key="Statistic:EnergyGrid:Month",
|
||||
name="Energy to Grid Month",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
PlenticoreSensorEntityDescription(
|
||||
module_id="_virt_",
|
||||
key="Statistic:EnergyGrid:Day",
|
||||
name="Energy to Grid Day",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
formatter="format_energy",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -252,8 +252,9 @@ turn_on:
|
||||
- light.ColorMode.RGBWW
|
||||
selector:
|
||||
color_temp:
|
||||
min_mireds: 153
|
||||
max_mireds: 500
|
||||
unit: "mired"
|
||||
min: 153
|
||||
max: 500
|
||||
kelvin:
|
||||
filter:
|
||||
attribute:
|
||||
@@ -266,11 +267,10 @@ turn_on:
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
color_temp:
|
||||
unit: "kelvin"
|
||||
min: 2000
|
||||
max: 6500
|
||||
step: 100
|
||||
unit_of_measurement: K
|
||||
brightness:
|
||||
filter:
|
||||
attribute:
|
||||
@@ -637,11 +637,10 @@ toggle:
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
color_temp:
|
||||
unit: "kelvin"
|
||||
min: 2000
|
||||
max: 6500
|
||||
step: 100
|
||||
unit_of_measurement: K
|
||||
brightness:
|
||||
filter:
|
||||
attribute:
|
||||
|
||||
@@ -380,7 +380,11 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
else STATE_OPEN
|
||||
)
|
||||
else:
|
||||
state = STATE_CLOSED if self.state == STATE_CLOSING else STATE_OPEN
|
||||
state = (
|
||||
STATE_CLOSED
|
||||
if self.state in [STATE_CLOSED, STATE_CLOSING]
|
||||
else STATE_OPEN
|
||||
)
|
||||
elif payload == self._config[CONF_STATE_OPENING]:
|
||||
state = STATE_OPENING
|
||||
elif payload == self._config[CONF_STATE_CLOSING]:
|
||||
|
||||
@@ -553,8 +553,6 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
self._valid_preset_mode_or_raise(preset_mode)
|
||||
|
||||
mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
|
||||
|
||||
await self.async_publish(
|
||||
|
||||
@@ -367,13 +367,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
if brightness_supported(self.supported_color_modes):
|
||||
try:
|
||||
if brightness := values["brightness"]:
|
||||
scale = self._config[CONF_BRIGHTNESS_SCALE]
|
||||
self._attr_brightness = min(
|
||||
int(
|
||||
brightness # type: ignore[operator]
|
||||
/ float(self._config[CONF_BRIGHTNESS_SCALE])
|
||||
* 255
|
||||
),
|
||||
255,
|
||||
round(brightness * 255 / scale), # type: ignore[operator]
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["metar", "pynws"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pynws==1.5.1"]
|
||||
"requirements": ["pynws==1.6.0"]
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ async def async_setup_entry(
|
||||
|
||||
def _check_for_recording_entry(api: PhilipsTV, entry: str, value: str) -> bool:
|
||||
"""Return True if at least one specified value is available within entry of list."""
|
||||
if api.recordings_list is None:
|
||||
return False
|
||||
for rec in api.recordings_list["recordings"]:
|
||||
if rec.get(entry) == value:
|
||||
return True
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.todo import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -31,7 +32,7 @@ async def async_setup_entry(
|
||||
"""Set up the Picnic shopping cart todo platform config entry."""
|
||||
picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR]
|
||||
|
||||
async_add_entities([PicnicCart(hass, picnic_coordinator, config_entry)])
|
||||
async_add_entities([PicnicCart(picnic_coordinator, config_entry)])
|
||||
|
||||
|
||||
class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
@@ -44,7 +45,6 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: PicnicUpdateCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
@@ -56,7 +56,6 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
manufacturer="Picnic",
|
||||
model=config_entry.unique_id,
|
||||
)
|
||||
self.hass = hass
|
||||
self._attr_unique_id = f"{config_entry.unique_id}-cart"
|
||||
|
||||
@property
|
||||
@@ -87,7 +86,7 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
)
|
||||
|
||||
if not product_id:
|
||||
raise ValueError("No product found or no product ID given")
|
||||
raise ServiceValidationError("No product found or no product ID given")
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.picnic_api_client.add_product, product_id, 1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Pushbullet Notification provider."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
@@ -10,7 +11,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from .const import DATA_UPDATED
|
||||
|
||||
|
||||
class PushBulletNotificationProvider(Listener):
|
||||
class PushBulletNotificationProvider(Listener): # type: ignore[misc]
|
||||
"""Provider for an account, leading to one or more sensors."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None:
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .api import PushBulletNotificationProvider
|
||||
from .const import ATTR_FILE, ATTR_FILE_URL, ATTR_URL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,8 +35,10 @@ async def async_get_service(
|
||||
"""Get the Pushbullet notification service."""
|
||||
if TYPE_CHECKING:
|
||||
assert discovery_info is not None
|
||||
pushbullet: PushBullet = hass.data[DOMAIN][discovery_info["entry_id"]].pushbullet
|
||||
return PushBulletNotificationService(hass, pushbullet)
|
||||
pb_provider: PushBulletNotificationProvider = hass.data[DOMAIN][
|
||||
discovery_info["entry_id"]
|
||||
]
|
||||
return PushBulletNotificationService(hass, pb_provider.pushbullet)
|
||||
|
||||
|
||||
class PushBulletNotificationService(BaseNotificationService):
|
||||
@@ -120,7 +123,7 @@ class PushBulletNotificationService(BaseNotificationService):
|
||||
pusher: PushBullet,
|
||||
email: str | None = None,
|
||||
phonenumber: str | None = None,
|
||||
):
|
||||
) -> None:
|
||||
"""Create the message content."""
|
||||
kwargs = {"body": message, "title": title}
|
||||
if email:
|
||||
|
||||
@@ -183,7 +183,7 @@ def _async_fix_device_id(
|
||||
device_entry_map = {}
|
||||
migrations = {}
|
||||
for device_entry in device_entries:
|
||||
unique_id = next(iter(device_entry.identifiers))[1]
|
||||
unique_id = str(next(iter(device_entry.identifiers))[1])
|
||||
device_entry_map[unique_id] = device_entry
|
||||
if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id:
|
||||
migrations[unique_id] = f"{mac_address}{suffix}"
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -148,6 +149,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
firmware_coordinator=firmware_coordinator,
|
||||
)
|
||||
|
||||
cleanup_disconnected_cams(hass, config_entry.entry_id, host)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
@@ -175,3 +178,56 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
def cleanup_disconnected_cams(
|
||||
hass: HomeAssistant, config_entry_id: str, host: ReolinkHost
|
||||
) -> None:
|
||||
"""Clean-up disconnected camera channels or channels where a different model camera is connected."""
|
||||
if not host.api.is_nvr:
|
||||
return
|
||||
|
||||
device_reg = dr.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
|
||||
for device in devices:
|
||||
device_id = [
|
||||
dev_id[1].split("_ch")
|
||||
for dev_id in device.identifiers
|
||||
if dev_id[0] == DOMAIN
|
||||
][0]
|
||||
|
||||
if len(device_id) < 2:
|
||||
# Do not consider the NVR itself
|
||||
continue
|
||||
|
||||
ch = int(device_id[1])
|
||||
ch_model = host.api.camera_model(ch)
|
||||
remove = False
|
||||
if ch not in host.api.channels:
|
||||
remove = True
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, since no camera is connected to NVR channel %s anymore",
|
||||
device.name,
|
||||
ch,
|
||||
)
|
||||
if ch_model not in [device.model, "Unknown"]:
|
||||
remove = True
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, since the camera model connected to channel %s changed from %s to %s",
|
||||
device.name,
|
||||
ch,
|
||||
device.model,
|
||||
ch_model,
|
||||
)
|
||||
if not remove:
|
||||
continue
|
||||
|
||||
# clean entity and device registry
|
||||
entity_reg = er.async_get(hass)
|
||||
entities = er.async_entries_for_device(
|
||||
entity_reg, device.id, include_disabled_entities=True
|
||||
)
|
||||
for entity in entities:
|
||||
entity_reg.async_remove(entity.entity_id)
|
||||
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
@@ -10,6 +10,6 @@ LOGGER = logging.getLogger(__package__)
|
||||
class StookwijzerState(StrEnum):
|
||||
"""Stookwijzer states for sensor entity."""
|
||||
|
||||
CODE_YELLOW = "code_yellow"
|
||||
CODE_ORANGE = "code_orange"
|
||||
CODE_RED = "code_red"
|
||||
BLUE = "blauw"
|
||||
ORANGE = "oranje"
|
||||
RED = "rood"
|
||||
|
||||
@@ -24,9 +24,8 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"state": client.state,
|
||||
"last_updated": last_updated,
|
||||
"alert": client.alert,
|
||||
"air_quality_index": client.lki,
|
||||
"windspeed_bft": client.windspeed_bft,
|
||||
"windspeed_ms": client.windspeed_ms,
|
||||
"forecast": client.forecast,
|
||||
"lqi": client.lqi,
|
||||
"windspeed": client.windspeed,
|
||||
"weather": client.weather,
|
||||
"concentrations": client.concentrations,
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/stookwijzer",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["stookwijzer==1.4.2"]
|
||||
"requirements": ["stookwijzer==1.3.0"]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ async def async_setup_entry(
|
||||
class StookwijzerSensor(SensorEntity):
|
||||
"""Defines a Stookwijzer binary sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by atlasleefomgeving.nl"
|
||||
_attr_attribution = "Data provided by stookwijzer.nu"
|
||||
_attr_device_class = SensorDeviceClass.ENUM
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
@@ -43,9 +43,9 @@ class StookwijzerSensor(SensorEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{entry.entry_id}")},
|
||||
name="Stookwijzer",
|
||||
manufacturer="Atlas Leefomgeving",
|
||||
manufacturer="stookwijzer.nu",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
configuration_url="https://www.atlasleefomgeving.nl/stookwijzer",
|
||||
configuration_url="https://www.stookwijzer.nu",
|
||||
)
|
||||
|
||||
def update(self) -> None:
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"sensor": {
|
||||
"stookwijzer": {
|
||||
"state": {
|
||||
"code_yellow": "Code yellow",
|
||||
"code_orange": "Code orange",
|
||||
"code_red": "Code red"
|
||||
"blauw": "Blue",
|
||||
"oranje": "Orange",
|
||||
"rood": "Red"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,15 +282,6 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset_mode of the fan."""
|
||||
if self.preset_modes and preset_mode not in self.preset_modes:
|
||||
_LOGGER.error(
|
||||
"Received invalid preset_mode: %s for entity %s. Expected: %s",
|
||||
preset_mode,
|
||||
self.entity_id,
|
||||
self.preset_modes,
|
||||
)
|
||||
return
|
||||
|
||||
self._preset_mode = preset_mode
|
||||
|
||||
if self._set_preset_mode_script:
|
||||
|
||||
@@ -35,7 +35,7 @@ from .const import (
|
||||
ATTR_DESCRIPTION,
|
||||
ATTR_DUE,
|
||||
ATTR_DUE_DATE,
|
||||
ATTR_DUE_DATE_TIME,
|
||||
ATTR_DUE_DATETIME,
|
||||
DOMAIN,
|
||||
TodoItemStatus,
|
||||
TodoListEntityFeature,
|
||||
@@ -73,7 +73,7 @@ TODO_ITEM_FIELDS = [
|
||||
required_feature=TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
|
||||
),
|
||||
TodoItemFieldDescription(
|
||||
service_field=ATTR_DUE_DATE_TIME,
|
||||
service_field=ATTR_DUE_DATETIME,
|
||||
validation=vol.All(cv.datetime, dt_util.as_local),
|
||||
todo_item_field=ATTR_DUE,
|
||||
required_feature=TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
|
||||
@@ -89,9 +89,7 @@ TODO_ITEM_FIELDS = [
|
||||
TODO_ITEM_FIELD_SCHEMA = {
|
||||
vol.Optional(desc.service_field): desc.validation for desc in TODO_ITEM_FIELDS
|
||||
}
|
||||
TODO_ITEM_FIELD_VALIDATIONS = [
|
||||
cv.has_at_most_one_key(ATTR_DUE_DATE, ATTR_DUE_DATE_TIME)
|
||||
]
|
||||
TODO_ITEM_FIELD_VALIDATIONS = [cv.has_at_most_one_key(ATTR_DUE_DATE, ATTR_DUE_DATETIME)]
|
||||
|
||||
|
||||
def _validate_supported_features(
|
||||
|
||||
@@ -6,7 +6,7 @@ DOMAIN = "todo"
|
||||
|
||||
ATTR_DUE = "due"
|
||||
ATTR_DUE_DATE = "due_date"
|
||||
ATTR_DUE_DATE_TIME = "due_date_time"
|
||||
ATTR_DUE_DATETIME = "due_datetime"
|
||||
ATTR_DESCRIPTION = "description"
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ add_item:
|
||||
example: "2023-11-17"
|
||||
selector:
|
||||
date:
|
||||
due_date_time:
|
||||
due_datetime:
|
||||
example: "2023-11-17 13:30:00"
|
||||
selector:
|
||||
datetime:
|
||||
@@ -65,7 +65,7 @@ update_item:
|
||||
example: "2023-11-17"
|
||||
selector:
|
||||
date:
|
||||
due_date_time:
|
||||
due_datetime:
|
||||
example: "2023-11-17 13:30:00"
|
||||
selector:
|
||||
datetime:
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
"name": "Due date",
|
||||
"description": "The date the to-do item is expected to be completed."
|
||||
},
|
||||
"due_date_time": {
|
||||
"name": "Due date time",
|
||||
"due_datetime": {
|
||||
"name": "Due date and time",
|
||||
"description": "The date and time the to-do item is expected to be completed."
|
||||
},
|
||||
"description": {
|
||||
@@ -58,8 +58,8 @@
|
||||
"name": "Due date",
|
||||
"description": "The date the to-do item is expected to be completed."
|
||||
},
|
||||
"due_date_time": {
|
||||
"name": "Due date time",
|
||||
"due_datetime": {
|
||||
"name": "Due date and time",
|
||||
"description": "The date and time the to-do item is expected to be completed."
|
||||
},
|
||||
"description": {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""A todo platform for Todoist."""
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
import datetime
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
TodoItem,
|
||||
@@ -13,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TodoistCoordinator
|
||||
@@ -30,6 +32,24 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
def _task_api_data(item: TodoItem) -> dict[str, Any]:
|
||||
"""Convert a TodoItem to the set of add or update arguments."""
|
||||
item_data: dict[str, Any] = {}
|
||||
if summary := item.summary:
|
||||
item_data["content"] = summary
|
||||
if due := item.due:
|
||||
if isinstance(due, datetime.datetime):
|
||||
item_data["due"] = {
|
||||
"date": due.date().isoformat(),
|
||||
"datetime": due.isoformat(),
|
||||
}
|
||||
else:
|
||||
item_data["due"] = {"date": due.isoformat()}
|
||||
if description := item.description:
|
||||
item_data["description"] = description
|
||||
return item_data
|
||||
|
||||
|
||||
class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntity):
|
||||
"""A Todoist TodoListEntity."""
|
||||
|
||||
@@ -37,6 +57,9 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -66,11 +89,21 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||
status = TodoItemStatus.COMPLETED
|
||||
else:
|
||||
status = TodoItemStatus.NEEDS_ACTION
|
||||
due: datetime.date | datetime.datetime | None = None
|
||||
if task_due := task.due:
|
||||
if task_due.datetime:
|
||||
due = dt_util.as_local(
|
||||
datetime.datetime.fromisoformat(task_due.datetime)
|
||||
)
|
||||
elif task_due.date:
|
||||
due = datetime.date.fromisoformat(task_due.date)
|
||||
items.append(
|
||||
TodoItem(
|
||||
summary=task.content,
|
||||
uid=task.id,
|
||||
status=status,
|
||||
due=due,
|
||||
description=task.description or None, # Don't use empty string
|
||||
)
|
||||
)
|
||||
self._attr_todo_items = items
|
||||
@@ -81,7 +114,7 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||
if item.status != TodoItemStatus.NEEDS_ACTION:
|
||||
raise ValueError("Only active tasks may be created.")
|
||||
await self.coordinator.api.add_task(
|
||||
content=item.summary or "",
|
||||
**_task_api_data(item),
|
||||
project_id=self._project_id,
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -89,8 +122,8 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update a To-do item."""
|
||||
uid: str = cast(str, item.uid)
|
||||
if item.summary:
|
||||
await self.coordinator.api.update_task(task_id=uid, content=item.summary)
|
||||
if update_data := _task_api_data(item):
|
||||
await self.coordinator.api.update_task(task_id=uid, **update_data)
|
||||
if item.status is not None:
|
||||
if item.status == TodoItemStatus.COMPLETED:
|
||||
await self.coordinator.api.close_task(task_id=uid)
|
||||
|
||||
@@ -119,8 +119,7 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
if not self._device_control:
|
||||
return
|
||||
|
||||
if not preset_mode == ATTR_AUTO:
|
||||
raise ValueError("Preset must be 'Auto'.")
|
||||
# Preset must be 'Auto'
|
||||
|
||||
await self._api(self._device_control.turn_on_auto_mode())
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
from pytrafikverket.trafikverket_camera import TrafikverketCamera
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -42,13 +42,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
web_session = async_get_clientsession(hass)
|
||||
camera_api = TrafikverketCamera(web_session, api_key)
|
||||
# Change entry unique id from location to camera id
|
||||
if entry.version == 1:
|
||||
location = entry.data[CONF_LOCATION]
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
|
||||
web_session = async_get_clientsession(hass)
|
||||
camera_api = TrafikverketCamera(web_session, api_key)
|
||||
|
||||
try:
|
||||
camera_info = await camera_api.async_get_camera(location)
|
||||
@@ -60,14 +59,40 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
if camera_id := camera_info.camera_id:
|
||||
entry.version = 2
|
||||
_LOGGER.debug(
|
||||
"Migrate Trafikverket Camera config entry unique id to %s",
|
||||
camera_id,
|
||||
)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
unique_id=f"{DOMAIN}-{camera_id}",
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migrated Trafikverket Camera config entry unique id to %s",
|
||||
camera_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Could not migrate the config entry. Camera has no id")
|
||||
return False
|
||||
|
||||
# Change entry data from location to id
|
||||
if entry.version == 2:
|
||||
location = entry.data[CONF_LOCATION]
|
||||
|
||||
try:
|
||||
camera_info = await camera_api.async_get_camera(location)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.error(
|
||||
"Could not migrate the config entry. No connection to the api"
|
||||
)
|
||||
return False
|
||||
|
||||
if camera_id := camera_info.camera_id:
|
||||
entry.version = 3
|
||||
_LOGGER.debug(
|
||||
"Migrate Trafikverket Camera config entry unique id to %s",
|
||||
camera_id,
|
||||
)
|
||||
new_data = entry.data.copy()
|
||||
new_data.pop(CONF_LOCATION)
|
||||
new_data[CONF_ID] = camera_id
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
return True
|
||||
_LOGGER.error("Could not migrate the config entry. Camera has no id")
|
||||
return False
|
||||
|
||||
@@ -14,7 +14,7 @@ from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -25,7 +25,7 @@ from .const import CONF_LOCATION, DOMAIN
|
||||
class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Trafikverket Camera integration."""
|
||||
|
||||
VERSION = 2
|
||||
VERSION = 3
|
||||
|
||||
entry: config_entries.ConfigEntry | None
|
||||
|
||||
@@ -53,10 +53,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if camera_info:
|
||||
camera_id = camera_info.camera_id
|
||||
if _location := camera_info.location:
|
||||
camera_location = _location
|
||||
else:
|
||||
camera_location = camera_info.camera_name
|
||||
camera_location = camera_info.camera_name or "Trafikverket Camera"
|
||||
|
||||
return (errors, camera_location, camera_id)
|
||||
|
||||
@@ -76,9 +73,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
|
||||
assert self.entry is not None
|
||||
errors, _, _ = await self.validate_input(
|
||||
api_key, self.entry.data[CONF_LOCATION]
|
||||
)
|
||||
errors, _, _ = await self.validate_input(api_key, self.entry.data[CONF_ID])
|
||||
|
||||
if not errors:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
@@ -121,10 +116,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=camera_location,
|
||||
data={
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_LOCATION: camera_location,
|
||||
},
|
||||
data={CONF_API_KEY: api_key, CONF_ID: camera_id},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -15,13 +15,13 @@ from pytrafikverket.exceptions import (
|
||||
from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_LOCATION, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
@@ -48,14 +48,14 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[CameraData]):
|
||||
)
|
||||
self.session = async_get_clientsession(hass)
|
||||
self._camera_api = TrafikverketCamera(self.session, entry.data[CONF_API_KEY])
|
||||
self._location = entry.data[CONF_LOCATION]
|
||||
self._id = entry.data[CONF_ID]
|
||||
|
||||
async def _async_update_data(self) -> CameraData:
|
||||
"""Fetch data from Trafikverket."""
|
||||
camera_data: CameraInfo
|
||||
image: bytes | None = None
|
||||
try:
|
||||
camera_data = await self._camera_api.async_get_camera(self._location)
|
||||
camera_data = await self._camera_api.async_get_camera(self._id)
|
||||
except (NoCameraFound, MultipleCamerasFound, UnknownError) as error:
|
||||
raise UpdateFailed from error
|
||||
except InvalidAuthentication as error:
|
||||
|
||||
@@ -338,6 +338,7 @@ class DPCode(StrEnum):
|
||||
TEMP_VALUE_V2 = "temp_value_v2"
|
||||
TEMPER_ALARM = "temper_alarm" # Tamper alarm
|
||||
TIME_TOTAL = "time_total"
|
||||
TIME_USE = "time_use" # Total seconds of irrigation
|
||||
TOTAL_CLEAN_AREA = "total_clean_area"
|
||||
TOTAL_CLEAN_COUNT = "total_clean_count"
|
||||
TOTAL_CLEAN_TIME = "total_clean_time"
|
||||
@@ -362,6 +363,7 @@ class DPCode(StrEnum):
|
||||
WATER_RESET = "water_reset" # Resetting of water usage days
|
||||
WATER_SET = "water_set" # Water level
|
||||
WATERSENSOR_STATE = "watersensor_state"
|
||||
WEATHER_DELAY = "weather_delay"
|
||||
WET = "wet" # Humidification
|
||||
WINDOW_CHECK = "window_check"
|
||||
WINDOW_STATE = "window_state"
|
||||
|
||||
@@ -75,6 +75,16 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = {
|
||||
icon="mdi:thermometer-lines",
|
||||
),
|
||||
),
|
||||
# Smart Water Timer
|
||||
"sfkzq": (
|
||||
# Irrigation will not be run within this set delay period
|
||||
SelectEntityDescription(
|
||||
key=DPCode.WEATHER_DELAY,
|
||||
translation_key="weather_delay",
|
||||
icon="mdi:weather-cloudy-clock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Siren Alarm
|
||||
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
|
||||
"sgbj": (
|
||||
|
||||
@@ -517,6 +517,18 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
),
|
||||
*BATTERY_SENSORS,
|
||||
),
|
||||
# Smart Water Timer
|
||||
"sfkzq": (
|
||||
# Total seconds of irrigation. Read-write value; the device appears to ignore the write action (maybe firmware bug)
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.TIME_USE,
|
||||
translation_key="total_watering_time",
|
||||
icon="mdi:history",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
*BATTERY_SENSORS,
|
||||
),
|
||||
# Water Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli
|
||||
"sj": BATTERY_SENSORS,
|
||||
|
||||
@@ -421,6 +421,19 @@
|
||||
"4": "Mood 4",
|
||||
"5": "Mood 5"
|
||||
}
|
||||
},
|
||||
"weather_delay": {
|
||||
"name": "Weather delay",
|
||||
"state": {
|
||||
"cancel": "Cancel",
|
||||
"24h": "24h",
|
||||
"48h": "48h",
|
||||
"72h": "72h",
|
||||
"96h": "96h",
|
||||
"120h": "120h",
|
||||
"144h": "144h",
|
||||
"168h": "168h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -556,6 +569,9 @@
|
||||
"water_level": {
|
||||
"name": "Water level"
|
||||
},
|
||||
"total_watering_time": {
|
||||
"name": "Total watering time"
|
||||
},
|
||||
"filter_utilization": {
|
||||
"name": "Filter utilization"
|
||||
},
|
||||
|
||||
@@ -430,6 +430,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Smart Water Timer
|
||||
"sfkzq": (
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.SWITCH,
|
||||
translation_key="switch",
|
||||
icon="mdi:sprinkler-variant",
|
||||
),
|
||||
),
|
||||
# Siren Alarm
|
||||
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
|
||||
"sgbj": (
|
||||
|
||||
@@ -11,11 +11,7 @@ from vallox_websocket_api import (
|
||||
ValloxInvalidInputException,
|
||||
)
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity,
|
||||
FanEntityFeature,
|
||||
NotValidPresetModeError,
|
||||
)
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -200,12 +196,6 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
|
||||
|
||||
Returns true if the mode has been changed, false otherwise.
|
||||
"""
|
||||
try:
|
||||
self._valid_preset_mode_or_raise(preset_mode)
|
||||
|
||||
except NotValidPresetModeError as err:
|
||||
raise ValueError(f"Not valid preset mode: {preset_mode}") from err
|
||||
|
||||
if preset_mode == self.preset_mode:
|
||||
return False
|
||||
|
||||
|
||||
@@ -283,8 +283,8 @@
|
||||
}
|
||||
},
|
||||
"water_heater": {
|
||||
"water": {
|
||||
"name": "Water"
|
||||
"domestic_hot_water": {
|
||||
"name": "Domestic hot water"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -64,13 +64,13 @@ def _build_entities(
|
||||
api: PyViCareDevice,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
) -> list[ViCareWater]:
|
||||
"""Create ViCare water entities for a device."""
|
||||
"""Create ViCare domestic hot water entities for a device."""
|
||||
return [
|
||||
ViCareWater(
|
||||
api,
|
||||
circuit,
|
||||
device_config,
|
||||
"water",
|
||||
"domestic_hot_water",
|
||||
)
|
||||
for circuit in get_circuits(api)
|
||||
]
|
||||
@@ -81,7 +81,7 @@ async def async_setup_entry(
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the ViCare climate platform."""
|
||||
"""Set up the ViCare water heater platform."""
|
||||
api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API]
|
||||
device_config = hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG]
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import asyncio
|
||||
from collections import deque
|
||||
from collections.abc import AsyncIterable, MutableSequence, Sequence
|
||||
from functools import partial
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
import wave
|
||||
|
||||
from voip_utils import (
|
||||
CallInfo,
|
||||
@@ -285,7 +287,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
|
||||
),
|
||||
conversation_id=self._conversation_id,
|
||||
device_id=self.voip_device.device_id,
|
||||
tts_audio_output="raw",
|
||||
tts_audio_output="wav",
|
||||
)
|
||||
|
||||
if self._pipeline_error:
|
||||
@@ -387,11 +389,16 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
|
||||
self._conversation_id = event.data["intent_output"]["conversation_id"]
|
||||
elif event.type == PipelineEventType.TTS_END:
|
||||
# Send TTS audio to caller over RTP
|
||||
media_id = event.data["tts_output"]["media_id"]
|
||||
self.hass.async_create_background_task(
|
||||
self._send_tts(media_id),
|
||||
"voip_pipeline_tts",
|
||||
)
|
||||
tts_output = event.data["tts_output"]
|
||||
if tts_output:
|
||||
media_id = tts_output["media_id"]
|
||||
self.hass.async_create_background_task(
|
||||
self._send_tts(media_id),
|
||||
"voip_pipeline_tts",
|
||||
)
|
||||
else:
|
||||
# Empty TTS response
|
||||
self._tts_done.set()
|
||||
elif event.type == PipelineEventType.ERROR:
|
||||
# Play error tone instead of wait for TTS
|
||||
self._pipeline_error = True
|
||||
@@ -402,11 +409,32 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
_extension, audio_bytes = await tts.async_get_media_source_audio(
|
||||
extension, data = await tts.async_get_media_source_audio(
|
||||
self.hass,
|
||||
media_id,
|
||||
)
|
||||
|
||||
if extension != "wav":
|
||||
raise ValueError(f"Only WAV audio can be streamed, got {extension}")
|
||||
|
||||
with io.BytesIO(data) as wav_io:
|
||||
with wave.open(wav_io, "rb") as wav_file:
|
||||
sample_rate = wav_file.getframerate()
|
||||
sample_width = wav_file.getsampwidth()
|
||||
sample_channels = wav_file.getnchannels()
|
||||
|
||||
if (
|
||||
(sample_rate != 16000)
|
||||
or (sample_width != 2)
|
||||
or (sample_channels != 1)
|
||||
):
|
||||
raise ValueError(
|
||||
"Expected rate/width/channels as 16000/2/1,"
|
||||
" got {sample_rate}/{sample_width}/{sample_channels}}"
|
||||
)
|
||||
|
||||
audio_bytes = wav_file.readframes(wav_file.getnframes())
|
||||
|
||||
_LOGGER.debug("Sending %s byte(s) of audio", len(audio_bytes))
|
||||
|
||||
# Time out 1 second after TTS audio should be finished
|
||||
@@ -414,7 +442,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol):
|
||||
tts_seconds = tts_samples / RATE
|
||||
|
||||
async with asyncio.timeout(tts_seconds + self.tts_extra_timeout):
|
||||
# Assume TTS audio is 16Khz 16-bit mono
|
||||
# TTS audio is 16Khz 16-bit mono
|
||||
await self._async_send_audio(audio_bytes)
|
||||
except asyncio.TimeoutError as err:
|
||||
_LOGGER.warning("TTS timeout")
|
||||
|
||||
@@ -17,6 +17,7 @@ from .const import ( # noqa: F401
|
||||
ERR_INVALID_FORMAT,
|
||||
ERR_NOT_FOUND,
|
||||
ERR_NOT_SUPPORTED,
|
||||
ERR_SERVICE_VALIDATION_ERROR,
|
||||
ERR_TEMPLATE_ERROR,
|
||||
ERR_TIMEOUT,
|
||||
ERR_UNAUTHORIZED,
|
||||
|
||||
@@ -778,7 +778,22 @@ async def handle_execute_script(
|
||||
|
||||
context = connection.context(msg)
|
||||
script_obj = Script(hass, script_config, f"{const.DOMAIN} script", const.DOMAIN)
|
||||
script_result = await script_obj.async_run(msg.get("variables"), context=context)
|
||||
try:
|
||||
script_result = await script_obj.async_run(
|
||||
msg.get("variables"), context=context
|
||||
)
|
||||
except ServiceValidationError as err:
|
||||
connection.logger.error(err)
|
||||
connection.logger.debug("", exc_info=err)
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
const.ERR_SERVICE_VALIDATION_ERROR,
|
||||
str(err),
|
||||
translation_domain=err.translation_domain,
|
||||
translation_key=err.translation_key,
|
||||
translation_placeholders=err.translation_placeholders,
|
||||
)
|
||||
return
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
|
||||
@@ -255,7 +255,10 @@ class ActiveConnection:
|
||||
log_handler = self.logger.error
|
||||
|
||||
code = const.ERR_UNKNOWN_ERROR
|
||||
err_message = None
|
||||
err_message: str | None = None
|
||||
translation_domain: str | None = None
|
||||
translation_key: str | None = None
|
||||
translation_placeholders: dict[str, Any] | None = None
|
||||
|
||||
if isinstance(err, Unauthorized):
|
||||
code = const.ERR_UNAUTHORIZED
|
||||
@@ -268,6 +271,10 @@ class ActiveConnection:
|
||||
err_message = "Timeout"
|
||||
elif isinstance(err, HomeAssistantError):
|
||||
err_message = str(err)
|
||||
code = const.ERR_HOME_ASSISTANT_ERROR
|
||||
translation_domain = err.translation_domain
|
||||
translation_key = err.translation_key
|
||||
translation_placeholders = err.translation_placeholders
|
||||
|
||||
# This if-check matches all other errors but also matches errors which
|
||||
# result in an empty message. In that case we will also log the stack
|
||||
@@ -276,7 +283,16 @@ class ActiveConnection:
|
||||
err_message = "Unknown error"
|
||||
log_handler = self.logger.exception
|
||||
|
||||
self.send_message(messages.error_message(msg["id"], code, err_message))
|
||||
self.send_message(
|
||||
messages.error_message(
|
||||
msg["id"],
|
||||
code,
|
||||
err_message,
|
||||
translation_domain=translation_domain,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
)
|
||||
|
||||
if code:
|
||||
err_message += f" ({code})"
|
||||
|
||||
@@ -66,7 +66,7 @@ def get_event_name(category: WorkoutCategory) -> str:
|
||||
|
||||
|
||||
class WithingsWorkoutCalendarEntity(
|
||||
CalendarEntity, WithingsEntity[WithingsWorkoutDataUpdateCoordinator]
|
||||
WithingsEntity[WithingsWorkoutDataUpdateCoordinator], CalendarEntity
|
||||
):
|
||||
"""A calendar entity."""
|
||||
|
||||
|
||||
@@ -530,9 +530,6 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
if await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
@@ -623,9 +620,6 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
if await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
@@ -721,9 +715,6 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
if await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
@@ -809,9 +800,6 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan. This method is a coroutine."""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
if await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
@@ -958,10 +946,6 @@ class XiaomiFan(XiaomiGenericFan):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
|
||||
if preset_mode == ATTR_MODE_NATURE:
|
||||
await self._try_command(
|
||||
"Setting natural fan speed percentage of the miio device failed.",
|
||||
@@ -1034,9 +1018,6 @@ class XiaomiFanP5(XiaomiGenericFan):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
@@ -1093,9 +1074,6 @@ class XiaomiFanMiot(XiaomiGenericFan):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zamg",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["zamg==0.3.0"]
|
||||
"requirements": ["zamg==0.3.3"]
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import re
|
||||
import voluptuous as vol
|
||||
from zhaquirks import setup as setup_quirks
|
||||
from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH
|
||||
from zigpy.exceptions import NetworkSettingsInconsistent
|
||||
from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
@@ -29,6 +29,7 @@ from .core.const import (
|
||||
CONF_CUSTOM_QUIRKS_PATH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONF_ENABLE_QUIRKS,
|
||||
CONF_FLOW_CONTROL,
|
||||
CONF_RADIO_TYPE,
|
||||
CONF_USB_PATH,
|
||||
CONF_ZIGPY,
|
||||
@@ -36,6 +37,8 @@ from .core.const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
STARTUP_FAILURE_DELAY_S,
|
||||
STARTUP_RETRIES,
|
||||
RadioType,
|
||||
)
|
||||
from .core.device import get_device_automation_triggers
|
||||
@@ -158,42 +161,67 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
_LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache)
|
||||
|
||||
zha_gateway = ZHAGateway(hass, zha_data.yaml_config, config_entry)
|
||||
# Retry setup a few times before giving up to deal with missing serial ports in VMs
|
||||
for attempt in range(STARTUP_RETRIES):
|
||||
try:
|
||||
zha_gateway = await ZHAGateway.async_from_config(
|
||||
hass=hass,
|
||||
config=zha_data.yaml_config,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
break
|
||||
except NetworkSettingsInconsistent as exc:
|
||||
await warn_on_inconsistent_network_settings(
|
||||
hass,
|
||||
config_entry=config_entry,
|
||||
old_state=exc.old_state,
|
||||
new_state=exc.new_state,
|
||||
)
|
||||
raise ConfigEntryError(
|
||||
"Network settings do not match most recent backup"
|
||||
) from exc
|
||||
except TransientConnectionError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
_LOGGER.debug(
|
||||
"Couldn't start coordinator (attempt %s of %s)",
|
||||
attempt + 1,
|
||||
STARTUP_RETRIES,
|
||||
exc_info=exc,
|
||||
)
|
||||
|
||||
try:
|
||||
await zha_gateway.async_initialize()
|
||||
except NetworkSettingsInconsistent as exc:
|
||||
await warn_on_inconsistent_network_settings(
|
||||
hass,
|
||||
config_entry=config_entry,
|
||||
old_state=exc.old_state,
|
||||
new_state=exc.new_state,
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
"Network settings do not match most recent backup"
|
||||
) from exc
|
||||
except Exception:
|
||||
if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp:
|
||||
try:
|
||||
await warn_on_wrong_silabs_firmware(
|
||||
hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
)
|
||||
except AlreadyRunningEZSP as exc:
|
||||
# If connecting fails but we somehow probe EZSP (e.g. stuck in the
|
||||
# bootloader), reconnect, it should work
|
||||
raise ConfigEntryNotReady from exc
|
||||
if attempt < STARTUP_RETRIES - 1:
|
||||
await asyncio.sleep(STARTUP_FAILURE_DELAY_S)
|
||||
continue
|
||||
|
||||
raise
|
||||
if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp:
|
||||
try:
|
||||
# Ignore all exceptions during probing, they shouldn't halt setup
|
||||
await warn_on_wrong_silabs_firmware(
|
||||
hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
)
|
||||
except AlreadyRunningEZSP as ezsp_exc:
|
||||
raise ConfigEntryNotReady from ezsp_exc
|
||||
|
||||
raise
|
||||
|
||||
repairs.async_delete_blocking_issues(hass)
|
||||
|
||||
manufacturer = zha_gateway.state.node_info.manufacturer
|
||||
model = zha_gateway.state.node_info.model
|
||||
|
||||
if manufacturer is None and model is None:
|
||||
manufacturer = "Unknown"
|
||||
model = "Unknown"
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))},
|
||||
identifiers={(DOMAIN, str(zha_gateway.coordinator_ieee))},
|
||||
connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.state.node_info.ieee))},
|
||||
identifiers={(DOMAIN, str(zha_gateway.state.node_info.ieee))},
|
||||
name="Zigbee Coordinator",
|
||||
manufacturer="ZHA",
|
||||
model=zha_gateway.radio_description,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
sw_version=zha_gateway.state.node_info.version,
|
||||
)
|
||||
|
||||
websocket_api.async_load_api(hass)
|
||||
@@ -267,5 +295,23 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
config_entry.version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, data=data)
|
||||
|
||||
if config_entry.version == 3:
|
||||
data = {**config_entry.data}
|
||||
|
||||
if not data[CONF_DEVICE].get(CONF_BAUDRATE):
|
||||
data[CONF_DEVICE][CONF_BAUDRATE] = {
|
||||
"deconz": 38400,
|
||||
"xbee": 57600,
|
||||
"ezsp": 57600,
|
||||
"znp": 115200,
|
||||
"zigate": 115200,
|
||||
}[data[CONF_RADIO_TYPE]]
|
||||
|
||||
if not data[CONF_DEVICE].get(CONF_FLOW_CONTROL):
|
||||
data[CONF_DEVICE][CONF_FLOW_CONTROL] = None
|
||||
|
||||
config_entry.version = 4
|
||||
hass.config_entries.async_update_entry(config_entry, data=data)
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
return True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user