forked from home-assistant/core
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1fb77cb4d | |||
| 95606135a6 | |||
| 47d9879c0c | |||
| e3c111b1dd | |||
| 9937743863 | |||
| ed3daed869 | |||
| 5d5dc24b33 | |||
| c39d6f0730 | |||
| 87ffd5ac56 | |||
| 71877fdeda | |||
| 2434a22e4e | |||
| 618fa08ab2 | |||
| 96003e3562 | |||
| 411e55d059 | |||
| 58533f02af | |||
| aa14793479 | |||
| 0191d3e41b | |||
| 319f76cdc8 | |||
| 530725bbfa | |||
| d8ae7d6955 | |||
| 3d0bafbdc9 | |||
| ef8e54877f | |||
| a39e1a6428 | |||
| 450be67406 | |||
| 25289e0ca1 | |||
| d983fa6da7 | |||
| b61397656c | |||
| 590546a9a5 | |||
| 9ba4d26abd | |||
| aa33da546d | |||
| 3845523a27 | |||
| 6a7fad0228 | |||
| 33f07ce035 | |||
| 4302c5c273 | |||
| b2df1b1c03 | |||
| 0aa134459b | |||
| 0ca3700c16 | |||
| 35ff633d99 | |||
| 7a2f6ce430 | |||
| 7cb603a226 | |||
| 43562289e4 | |||
| 79fa7caa41 | |||
| 8bdb27c88b | |||
| f676448f27 | |||
| 639c4a843b | |||
| 02dee34338 | |||
| 4e0290ce0e | |||
| fa2f49693c | |||
| 2ce784105d | |||
| 85fb4a27a3 | |||
| 8cbedbe26b | |||
| 5bd52da13a | |||
| d53848aae4 | |||
| 4e0d6f287e | |||
| 5af5f3694e | |||
| b539b25682 | |||
| ca31479d29 | |||
| 92dfec3c98 | |||
| 230c29edbe | |||
| 559fe65471 | |||
| 384d10a51d | |||
| e5a620545c | |||
| 7b84e86f89 | |||
| 18b6de567d | |||
| a6076a0d33 | |||
| 7164993562 | |||
| bc21836e7e | |||
| 52612b10fd | |||
| 623d85ecaa | |||
| 43631d5944 | |||
| 112aab47fb | |||
| ea13f102e0 | |||
| bb33725e7f | |||
| bd6890ab83 | |||
| 25c611ffc4 | |||
| fc24b61859 | |||
| 71588b5c22 | |||
| 14dfb6a255 | |||
| ef97255d9c | |||
| e8afdd67d0 | |||
| 008e4413b5 | |||
| c373d40e34 | |||
| bdf51553ef | |||
| f2edc15687 | |||
| 286a09d737 | |||
| e8ee2fd25c | |||
| 11b8b01cde | |||
| 4f761c25d8 | |||
| 953ceb0d8d | |||
| e53672250f | |||
| 84901f1983 | |||
| e4d973e8a2 | |||
| cdd7ce435a | |||
| c7ce53cc49 | |||
| db7d0a0ee9 | |||
| 906febadef | |||
| bc740f95c9 | |||
| bf4e527f44 | |||
| 35e582a240 | |||
| 65d25bd780 | |||
| b8a2c14813 |
@@ -93,6 +93,11 @@ from .util.async_ import create_eager_task
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_virtual_env
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||
from anyio._backends import _asyncio # noqa: F401
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.4.6"]
|
||||
"requirements": ["aioairzone-cloud==0.4.7"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
from time import monotonic
|
||||
|
||||
from aiohttp import ClientError
|
||||
from yalexs.activity import Activity, ActivityType
|
||||
@@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
||||
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
|
||||
|
||||
INITIAL_LOCK_RESYNC_TIME = 60
|
||||
|
||||
# If there is a storm of activity (ie lock, unlock, door open, door close, etc)
|
||||
# we want to debounce the updates so we don't hammer the activity api too much.
|
||||
ACTIVITY_DEBOUNCE_COOLDOWN = 3
|
||||
ACTIVITY_DEBOUNCE_COOLDOWN = 4
|
||||
|
||||
|
||||
@callback
|
||||
@@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
self.pubnub = pubnub
|
||||
self._update_debounce: dict[str, Debouncer] = {}
|
||||
self._update_debounce_jobs: dict[str, HassJob] = {}
|
||||
self._start_time: float | None = None
|
||||
|
||||
@callback
|
||||
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
|
||||
@@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Token refresh check and catch up the activity stream."""
|
||||
self._start_time = monotonic()
|
||||
update_debounce = self._update_debounce
|
||||
update_debounce_jobs = self._update_debounce_jobs
|
||||
for house_id in self._house_ids:
|
||||
@@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
|
||||
debouncer = self._update_debounce[house_id]
|
||||
debouncer.async_schedule_call()
|
||||
|
||||
# Schedule two updates past the debounce time
|
||||
# to ensure we catch the case where the activity
|
||||
# api does not update right away and we need to poll
|
||||
# it again. Sometimes the lock operator or a doorbell
|
||||
# will not show up in the activity stream right away.
|
||||
# Only do additional polls if we are past
|
||||
# the initial lock resync time to avoid a storm
|
||||
# of activity at setup.
|
||||
if (
|
||||
not self._start_time
|
||||
or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Skipping additional updates due to ongoing initial lock resync time"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug("Scheduling additional updates for house id %s", house_id)
|
||||
job = self._update_debounce_jobs[house_id]
|
||||
for step in (1, 2):
|
||||
future_updates.append(
|
||||
|
||||
@@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag"
|
||||
# Limit battery, online, and hardware updates to hourly
|
||||
# in order to reduce the number of api requests and
|
||||
# avoid hitting rate limits
|
||||
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
|
||||
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24)
|
||||
|
||||
# Activity needs to be checked more frequently as the
|
||||
# doorbell motion and rings are included here
|
||||
|
||||
@@ -49,9 +49,17 @@ class AugustSubscriberMixin:
|
||||
"""Call the refresh method."""
|
||||
self._hass.async_create_task(self._async_refresh(now), eager_start=True)
|
||||
|
||||
@callback
|
||||
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
|
||||
"""Cancel the scheduled update."""
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
|
||||
@callback
|
||||
def _async_setup_listeners(self) -> None:
|
||||
"""Create interval and stop listeners."""
|
||||
self._async_cancel_update_interval()
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self._hass,
|
||||
self._async_scheduled_refresh,
|
||||
@@ -59,17 +67,12 @@ class AugustSubscriberMixin:
|
||||
name="august refresh",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_cancel_update_interval(_: Event) -> None:
|
||||
self._stop_interval = None
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
|
||||
self._stop_interval = self._hass.bus.async_listen(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_async_cancel_update_interval,
|
||||
run_immediately=True,
|
||||
)
|
||||
if not self._stop_interval:
|
||||
self._stop_interval = self._hass.bus.async_listen(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
self._async_cancel_update_interval,
|
||||
run_immediately=True,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_unsubscribe_device_id(
|
||||
@@ -82,13 +85,7 @@ class AugustSubscriberMixin:
|
||||
|
||||
if self._subscriptions:
|
||||
return
|
||||
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
if self._stop_interval:
|
||||
self._stop_interval()
|
||||
self._stop_interval = None
|
||||
self._async_cancel_update_interval()
|
||||
|
||||
@callback
|
||||
def async_signal_device_id_update(self, device_id: str) -> None:
|
||||
|
||||
@@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
mjpeg_url=self.mjpeg_source,
|
||||
still_image_url=self.image_source,
|
||||
authentication=HTTP_DIGEST_AUTHENTICATION,
|
||||
verify_ssl=False,
|
||||
unique_id=f"{hub.unique_id}-camera",
|
||||
)
|
||||
|
||||
@@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
|
||||
Additionally used when device change IP address.
|
||||
"""
|
||||
proto = self.hub.config.protocol
|
||||
host = self.hub.config.host
|
||||
port = self.hub.config.port
|
||||
|
||||
image_options = self.generate_options(skip_stream_profile=True)
|
||||
self._still_image_url = (
|
||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
||||
f"/jpg/image.cgi{image_options}"
|
||||
f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}"
|
||||
)
|
||||
|
||||
mjpeg_options = self.generate_options()
|
||||
self._mjpeg_url = (
|
||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
||||
f"/mjpg/video.cgi{mjpeg_options}"
|
||||
f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}"
|
||||
)
|
||||
|
||||
stream_options = self.generate_options(add_video_codec_h264=True)
|
||||
@@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
self.hub.additional_diagnostics["camera_sources"] = {
|
||||
"Image": self._still_image_url,
|
||||
"MJPEG": self._mjpeg_url,
|
||||
"Stream": (
|
||||
f"rtsp://user:pass@{self.hub.config.host}/axis-media"
|
||||
f"/media.amp{stream_options}"
|
||||
),
|
||||
"Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"),
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
self, entry_data: Mapping[str, Any], keep_password: bool
|
||||
) -> ConfigFlowResult:
|
||||
"""Re-run configuration step."""
|
||||
protocol = entry_data.get(CONF_PROTOCOL, "http")
|
||||
password = entry_data[CONF_PASSWORD] if keep_password else ""
|
||||
self.discovery_schema = {
|
||||
vol.Required(
|
||||
CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http")
|
||||
): str,
|
||||
vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES),
|
||||
vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
|
||||
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
|
||||
vol.Required(
|
||||
CONF_PASSWORD,
|
||||
default=entry_data[CONF_PASSWORD] if keep_password else "",
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD, default=password): str,
|
||||
vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
@@ -31,6 +32,7 @@ class AxisConfig:
|
||||
|
||||
entry: ConfigEntry
|
||||
|
||||
protocol: str
|
||||
host: str
|
||||
port: int
|
||||
username: str
|
||||
@@ -54,6 +56,7 @@ class AxisConfig:
|
||||
options = config_entry.options
|
||||
return cls(
|
||||
entry=config_entry,
|
||||
protocol=config.get(CONF_PROTOCOL, "http"),
|
||||
host=config[CONF_HOST],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD],
|
||||
|
||||
@@ -116,7 +116,7 @@ class AxisHub:
|
||||
if status.status.state == ClientState.ACTIVE:
|
||||
self.config.entry.async_on_unload(
|
||||
await mqtt.async_subscribe(
|
||||
hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message
|
||||
hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message
|
||||
)
|
||||
)
|
||||
|
||||
@@ -124,7 +124,8 @@ class AxisHub:
|
||||
def mqtt_message(self, message: ReceiveMessage) -> None:
|
||||
"""Receive Axis MQTT message."""
|
||||
self.disconnect_from_stream()
|
||||
|
||||
if message.topic.endswith("event/connection"):
|
||||
return
|
||||
event = mqtt_json_to_event(message.payload)
|
||||
self.api.event.handler(event)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==59"],
|
||||
"requirements": ["axis==60"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
@@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_name,
|
||||
area=None,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
start,
|
||||
template,
|
||||
@@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
self.hass.bus.async_listen(
|
||||
ar.EVENT_AREA_REGISTRY_UPDATED,
|
||||
self._async_handle_area_registry_changed,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
fr.EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
@@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return lang_intents
|
||||
|
||||
@core.callback
|
||||
def _async_handle_area_registry_changed(
|
||||
self, event: core.Event[ar.EventAreaRegistryUpdatedData]
|
||||
def _async_handle_area_floor_registry_changed(
|
||||
self,
|
||||
event: core.Event[
|
||||
ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData
|
||||
],
|
||||
) -> None:
|
||||
"""Clear area area cache when the area registry has changed."""
|
||||
"""Clear area/floor list cache when the area registry has changed."""
|
||||
self._slot_lists = None
|
||||
|
||||
@core.callback
|
||||
@@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Default name
|
||||
entity_names.append((state.name, state.name, context))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
|
||||
# Expose all areas.
|
||||
#
|
||||
# We pass in area id here with the expectation that no two areas will
|
||||
@@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
area_names.append((alias, area.id))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
# Expose all floors.
|
||||
#
|
||||
# We pass in floor id here with the expectation that no two floors will
|
||||
# share the same name or alias.
|
||||
floors = fr.async_get(self.hass)
|
||||
floor_names = []
|
||||
for floor in floors.async_list_floors():
|
||||
floor_names.append((floor.name, floor.floor_id))
|
||||
if floor.aliases:
|
||||
for alias in floor.aliases:
|
||||
if not alias.strip():
|
||||
continue
|
||||
|
||||
floor_names.append((alias, floor.floor_id))
|
||||
|
||||
self._slot_lists = {
|
||||
"area": TextSlotList.from_tuples(area_names, allow_template=False),
|
||||
"name": TextSlotList.from_tuples(entity_names, allow_template=False),
|
||||
"floor": TextSlotList.from_tuples(floor_names, allow_template=False),
|
||||
}
|
||||
|
||||
return self._slot_lists
|
||||
@@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
# area only
|
||||
return ErrorKey.NO_AREA, {"area": unmatched_area}
|
||||
|
||||
if unmatched_floor := unmatched_text.get("floor"):
|
||||
# floor only
|
||||
return ErrorKey.NO_FLOOR, {"floor": unmatched_floor}
|
||||
|
||||
# Area may still have matched
|
||||
matched_area: str | None = None
|
||||
if matched_area_entity := result.entities.get("area"):
|
||||
@@ -1000,6 +1029,13 @@ def _get_no_states_matched_response(
|
||||
"area": no_states_error.area,
|
||||
}
|
||||
|
||||
if no_states_error.floor:
|
||||
# domain in floor
|
||||
return ErrorKey.NO_DOMAIN_IN_FLOOR, {
|
||||
"domain": domain,
|
||||
"floor": no_states_error.floor,
|
||||
}
|
||||
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.27"]
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["async-upnp-client==0.38.2"],
|
||||
"requirements": ["async-upnp-client==0.38.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -11,7 +11,11 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
@@ -43,6 +47,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
hass.async_create_task(_async_import_config(hass, config))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Import the Downloader component from the YAML file."""
|
||||
|
||||
import_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
@@ -51,28 +62,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
},
|
||||
)
|
||||
|
||||
translation_key = "deprecated_yaml"
|
||||
if (
|
||||
import_result["type"] == FlowResultType.ABORT
|
||||
and import_result["reason"] == "import_failed"
|
||||
and import_result["reason"] != "single_instance_allowed"
|
||||
):
|
||||
translation_key = "import_failed"
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.9.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
},
|
||||
)
|
||||
return True
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="directory_does_not_exist",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
"url": "/config/integrations/dashboard/add?domain=downloader",
|
||||
},
|
||||
)
|
||||
else:
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -83,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = hass.config.path(download_path)
|
||||
|
||||
if not os.path.isdir(download_path):
|
||||
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
"Download path %s does not exist. File Downloader not active", download_path
|
||||
)
|
||||
|
||||
@@ -46,19 +46,24 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by configuration file."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return await self.async_step_user(user_input)
|
||||
try:
|
||||
await self._validate_input(user_input)
|
||||
except DirectoryDoesNotExist:
|
||||
return self.async_abort(reason="directory_does_not_exist")
|
||||
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
|
||||
|
||||
async def _validate_input(self, user_input: dict[str, Any]) -> None:
|
||||
"""Validate the user input if the directory exists."""
|
||||
if not os.path.isabs(user_input[CONF_DOWNLOAD_DIR]):
|
||||
download_path = self.hass.config.path(user_input[CONF_DOWNLOAD_DIR])
|
||||
download_path = user_input[CONF_DOWNLOAD_DIR]
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = self.hass.config.path(download_path)
|
||||
|
||||
if not os.path.isdir(download_path):
|
||||
if not await self.hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
"Download path %s does not exist. File Downloader not active",
|
||||
download_path,
|
||||
|
||||
@@ -37,13 +37,9 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"import_failed": {
|
||||
"directory_does_not_exist": {
|
||||
"title": "The {integration_title} failed to import",
|
||||
"description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details."
|
||||
"description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"restart": "mdi:restart",
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"stop": "mdi:stop"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["flexit_bacnet==2.1.0"]
|
||||
"requirements": ["flexit_bacnet==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -311,6 +311,17 @@ class FritzBoxTools(
|
||||
)
|
||||
return unregister_entity_updates
|
||||
|
||||
def _entity_states_update(self) -> dict:
|
||||
"""Run registered entity update calls."""
|
||||
entity_states = {}
|
||||
for key in list(self._entity_update_functions):
|
||||
if (update_fn := self._entity_update_functions.get(key)) is not None:
|
||||
_LOGGER.debug("update entity %s", key)
|
||||
entity_states[key] = update_fn(
|
||||
self.fritz_status, self.data["entity_states"].get(key)
|
||||
)
|
||||
return entity_states
|
||||
|
||||
async def _async_update_data(self) -> UpdateCoordinatorDataType:
|
||||
"""Update FritzboxTools data."""
|
||||
entity_data: UpdateCoordinatorDataType = {
|
||||
@@ -319,15 +330,9 @@ class FritzBoxTools(
|
||||
}
|
||||
try:
|
||||
await self.async_scan_devices()
|
||||
for key in list(self._entity_update_functions):
|
||||
_LOGGER.debug("update entity %s", key)
|
||||
entity_data["entity_states"][
|
||||
key
|
||||
] = await self.hass.async_add_executor_job(
|
||||
self._entity_update_functions[key],
|
||||
self.fritz_status,
|
||||
self.data["entity_states"].get(key),
|
||||
)
|
||||
entity_data["entity_states"] = await self.hass.async_add_executor_job(
|
||||
self._entity_states_update
|
||||
)
|
||||
if self.has_call_deflections:
|
||||
entity_data[
|
||||
"call_deflections"
|
||||
|
||||
@@ -141,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
# update old and user-configured config entries
|
||||
for entry in self._async_current_entries():
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data[CONF_HOST] == host:
|
||||
if uuid and not entry.unique_id:
|
||||
self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240328.0"]
|
||||
"requirements": ["home-assistant-frontend==20240404.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.45", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.46", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -119,4 +119,5 @@ class PowerviewShadeButton(ShadeEntity, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.entity_description.press_action(self._shade)
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self.entity_description.press_action(self._shade)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -25,6 +26,10 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData])
|
||||
"""Initialize DataUpdateCoordinator to gather data for specific Powerview Hub."""
|
||||
self.shades = shades
|
||||
self.hub = hub
|
||||
# The hub tends to crash if there are multiple radio operations at the same time
|
||||
# but it seems to handle all other requests that do not use RF without issue
|
||||
# so we have a lock to prevent multiple radio operations at the same time
|
||||
self.radio_operation_lock = asyncio.Lock()
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
|
||||
@@ -67,7 +67,8 @@ async def async_setup_entry(
|
||||
|
||||
for shade in pv_entry.shade_data.values():
|
||||
_LOGGER.debug("Initial refresh of shade: %s", shade.name)
|
||||
await shade.refresh(suppress_timeout=True) # default 15 second timeout
|
||||
async with coordinator.radio_operation_lock:
|
||||
await shade.refresh(suppress_timeout=True) # default 15 second timeout
|
||||
|
||||
entities: list[ShadeEntity] = []
|
||||
for shade in pv_entry.shade_data.values():
|
||||
@@ -207,7 +208,8 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||
async def _async_execute_move(self, move: ShadePosition) -> None:
|
||||
"""Execute a move that can affect multiple positions."""
|
||||
_LOGGER.debug("Move request %s: %s", self.name, move)
|
||||
response = await self._shade.move(move)
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
response = await self._shade.move(move)
|
||||
_LOGGER.debug("Move response %s: %s", self.name, response)
|
||||
|
||||
# Process the response from the hub (including new positions)
|
||||
@@ -318,7 +320,10 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||
# error if are already have one in flight
|
||||
return
|
||||
# suppress timeouts caused by hub nightly reboot
|
||||
await self._shade.refresh(suppress_timeout=True) # default 15 second timeout
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self._shade.refresh(
|
||||
suppress_timeout=True
|
||||
) # default 15 second timeout
|
||||
_LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position)
|
||||
self._async_update_shade_data(self._shade.current_position)
|
||||
|
||||
|
||||
@@ -114,5 +114,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity):
|
||||
"""Change the selected option."""
|
||||
await self.entity_description.select_fn(self._shade, option)
|
||||
# force update data to ensure new info is in coordinator
|
||||
await self._shade.refresh()
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self._shade.refresh(suppress_timeout=True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -153,5 +153,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh sensor entity."""
|
||||
await self.entity_description.update_fn(self._shade)
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self.entity_description.update_fn(self._shade)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"select_next": "mdi:skip",
|
||||
"select_next": "mdi:skip-next",
|
||||
"select_option": "mdi:check",
|
||||
"select_previous": "mdi:skip-previous",
|
||||
"select_first": "mdi:skip-backward",
|
||||
|
||||
@@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path):
|
||||
"views": views,
|
||||
}
|
||||
|
||||
if config is None:
|
||||
if config is None or "views" not in config:
|
||||
return data
|
||||
|
||||
for idx, view in enumerate(config["views"]):
|
||||
|
||||
@@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity):
|
||||
else:
|
||||
brightness = self._prev_brightness
|
||||
self._prev_brightness = brightness
|
||||
args = {"new_level": brightness}
|
||||
args = {"new_level": to_lutron_level(brightness)}
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
|
||||
self._lutron_device.set_level(**args)
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"unjoin": "mdi:ungroup",
|
||||
"volume_down": "mdi:volume-minus",
|
||||
"volume_mute": "mdi:volume-mute",
|
||||
"volume_set": "mdi:volume",
|
||||
"volume_set": "mdi:volume-medium",
|
||||
"volume_up": "mdi:volume-plus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Motionblinds BLE integration."""
|
||||
"""Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -34,9 +34,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Motionblinds BLE integration."""
|
||||
"""Set up Motionblinds Bluetooth integration."""
|
||||
|
||||
_LOGGER.debug("Setting up Motionblinds BLE integration")
|
||||
_LOGGER.debug("Setting up Motionblinds Bluetooth integration")
|
||||
|
||||
# The correct time is needed for encryption
|
||||
_LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone)
|
||||
@@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Motionblinds BLE device from a config entry."""
|
||||
"""Set up Motionblinds Bluetooth device from a config entry."""
|
||||
|
||||
_LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE])
|
||||
|
||||
@@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Motionblinds BLE device from a config entry."""
|
||||
"""Unload Motionblinds Bluetooth device from a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Button entities for the Motionblinds BLE integration."""
|
||||
"""Button entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Config flow for Motionblinds BLE integration."""
|
||||
"""Config flow for Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str})
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Motionblinds BLE."""
|
||||
"""Handle a config flow for Motionblinds Bluetooth."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a ConfigFlow."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Constants for the Motionblinds BLE integration."""
|
||||
"""Constants for the Motionblinds Bluetooth integration."""
|
||||
|
||||
ATTR_CONNECT = "connect"
|
||||
ATTR_DISCONNECT = "disconnect"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cover entities for the Motionblinds BLE integration."""
|
||||
"""Cover entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Base entities for the Motionblinds BLE integration."""
|
||||
"""Base entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MotionblindsBLEEntity(Entity):
|
||||
"""Base class for Motionblinds BLE entities."""
|
||||
"""Base class for Motionblinds Bluetooth entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "motionblinds_ble",
|
||||
"name": "Motionblinds BLE",
|
||||
"name": "Motionblinds Bluetooth",
|
||||
"bluetooth": [
|
||||
{
|
||||
"local_name": "MOTION_*",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Select entities for the Motionblinds BLE integration."""
|
||||
"""Select entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from myuplink import MyUplinkAPI, get_manufacturer, get_system_name
|
||||
from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
@@ -92,7 +92,7 @@ def create_devices(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=get_system_name(system),
|
||||
manufacturer=get_manufacturer(device),
|
||||
model=device.productName,
|
||||
model=get_model(device),
|
||||
sw_version=device.firmwareCurrent,
|
||||
serial_number=device.product_serial_number,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/myuplink",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["myuplink==0.5.0"]
|
||||
"requirements": ["myuplink==0.6.0"]
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
(CONF_REFRESH_TOKEN, client.refresh_token),
|
||||
(CONF_USER_UUID, client.user_uuid),
|
||||
):
|
||||
if entry.data[key] == value:
|
||||
if entry.data.get(key) == value:
|
||||
continue
|
||||
entry_updates["data"][key] = value
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.1"]
|
||||
"requirements": ["opower==0.4.3"]
|
||||
}
|
||||
|
||||
@@ -105,6 +105,22 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
|
||||
)
|
||||
== 1,
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.CORE_HEATING_STATUS,
|
||||
name="Heating status",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE,
|
||||
name="Absence mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_BOOST_MODE,
|
||||
name="Boost mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
),
|
||||
]
|
||||
|
||||
SUPPORTED_STATES = {
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import cast
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeAssistantOverkizData
|
||||
@@ -27,15 +28,16 @@ async def async_setup_entry(
|
||||
"""Set up the Overkiz climate from a config entry."""
|
||||
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
# Match devices based on the widget.
|
||||
entities_based_on_widget: list[Entity] = [
|
||||
WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator)
|
||||
for device in data.platforms[Platform.CLIMATE]
|
||||
if device.widget in WIDGET_TO_CLIMATE_ENTITY
|
||||
)
|
||||
]
|
||||
|
||||
# Match devices based on the widget and controllableName
|
||||
# This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget.
|
||||
async_add_entities(
|
||||
# Match devices based on the widget and controllableName.
|
||||
# ie Atlantic APC
|
||||
entities_based_on_widget_and_controllable: list[Entity] = [
|
||||
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][
|
||||
cast(Controllable, device.controllable_name)
|
||||
](device.device_url, data.coordinator)
|
||||
@@ -43,14 +45,21 @@ async def async_setup_entry(
|
||||
if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY
|
||||
and device.controllable_name
|
||||
in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget]
|
||||
)
|
||||
]
|
||||
|
||||
# Hitachi Air To Air Heat Pumps
|
||||
async_add_entities(
|
||||
# Match devices based on the widget and protocol.
|
||||
# #ie Hitachi Air To Air Heat Pumps
|
||||
entities_based_on_widget_and_protocol: list[Entity] = [
|
||||
WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
for device in data.platforms[Platform.CLIMATE]
|
||||
if device.widget in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY
|
||||
and device.protocol in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget]
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
entities_based_on_widget
|
||||
+ entities_based_on_widget_and_controllable
|
||||
+ entities_based_on_widget_and_protocol
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
|
||||
await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode])
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
heating_mode = cast(
|
||||
str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE)
|
||||
@@ -179,7 +179,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
|
||||
return OVERKIZ_TO_PRESET_MODES[heating_mode]
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return hvac target temperature."""
|
||||
current_heating_profile = self.current_heating_profile
|
||||
if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE:
|
||||
|
||||
+306
-88
@@ -3,16 +3,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import sleep
|
||||
from functools import cached_property
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
|
||||
from homeassistant.components.climate import PRESET_NONE, HVACMode
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
PRESET_NONE,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES
|
||||
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..executor import OverkizExecutor
|
||||
from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone
|
||||
from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE
|
||||
|
||||
PRESET_SCHEDULE = "schedule"
|
||||
PRESET_MANUAL = "manual"
|
||||
@@ -24,32 +32,127 @@ OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = {
|
||||
|
||||
PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()}
|
||||
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 20
|
||||
# Maps the HVAC current ZoneControl system operating mode.
|
||||
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
|
||||
OverkizCommandParam.COOLING: HVACAction.COOLING,
|
||||
OverkizCommandParam.DRYING: HVACAction.DRYING,
|
||||
OverkizCommandParam.HEATING: HVACAction.HEATING,
|
||||
# There is no known way to differentiate OFF from Idle.
|
||||
OverkizCommandParam.STOP: HVACAction.OFF,
|
||||
}
|
||||
|
||||
HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE: dict[HVACAction, OverkizState] = {
|
||||
HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_PROFILE,
|
||||
HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_PROFILE,
|
||||
}
|
||||
|
||||
HVAC_ACTION_TO_OVERKIZ_MODE_STATE: dict[HVACAction, OverkizState] = {
|
||||
HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_MODE,
|
||||
HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_MODE,
|
||||
}
|
||||
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1
|
||||
|
||||
SUPPORTED_FEATURES: ClimateEntityFeature = (
|
||||
ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE: dict[
|
||||
OverkizCommandParam, tuple[HVACMode, ClimateEntityFeature]
|
||||
] = {
|
||||
OverkizCommandParam.COOLING: (
|
||||
HVACMode.COOL,
|
||||
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
),
|
||||
OverkizCommandParam.HEATING: (
|
||||
HVACMode.HEAT,
|
||||
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
),
|
||||
OverkizCommandParam.HEATING_AND_COOLING: (
|
||||
HVACMode.HEAT_COOL,
|
||||
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...)
|
||||
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...).
|
||||
class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
"""Representation of Atlantic Pass APC Heating And Cooling Zone Control."""
|
||||
|
||||
_attr_target_temperature_step = PRECISION_HALVES
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
|
||||
# There is less supported functions, because they depend on the ZoneControl.
|
||||
if not self.is_using_derogated_temperature_fallback:
|
||||
# Modes are not configurable, they will follow current HVAC Mode of Zone Control.
|
||||
self._attr_hvac_modes = []
|
||||
# When using derogated temperature, we fallback to legacy behavior.
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return
|
||||
|
||||
# Those are available and tested presets on Shogun.
|
||||
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
|
||||
self._attr_hvac_modes = []
|
||||
self._attr_supported_features = ClimateEntityFeature(0)
|
||||
|
||||
# Modes depends on device capabilities.
|
||||
if (thermal_configuration := self.thermal_configuration) is not None:
|
||||
(
|
||||
device_hvac_mode,
|
||||
climate_entity_feature,
|
||||
) = thermal_configuration
|
||||
self._attr_hvac_modes = [device_hvac_mode, HVACMode.OFF]
|
||||
self._attr_supported_features = climate_entity_feature
|
||||
|
||||
# Those are available and tested presets on Shogun.
|
||||
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
|
||||
|
||||
# Those APC Heating and Cooling probes depends on the zone control device (main probe).
|
||||
# Only the base device (#1) can be used to get/set some states.
|
||||
# Like to retrieve and set the current operating mode (heating, cooling, drying, off).
|
||||
self.zone_control_device = self.executor.linked_device(
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
|
||||
|
||||
self.zone_control_executor: OverkizExecutor | None = None
|
||||
|
||||
if (
|
||||
zone_control_device := self.executor.linked_device(
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
|
||||
)
|
||||
) is not None:
|
||||
self.zone_control_executor = OverkizExecutor(
|
||||
zone_control_device.device_url,
|
||||
coordinator,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def thermal_configuration(self) -> tuple[HVACMode, ClimateEntityFeature] | None:
|
||||
"""Retrieve thermal configuration for this devices."""
|
||||
|
||||
if (
|
||||
(
|
||||
state_thermal_configuration := cast(
|
||||
OverkizCommandParam | None,
|
||||
self.executor.select_state(OverkizState.CORE_THERMAL_CONFIGURATION),
|
||||
)
|
||||
)
|
||||
is not None
|
||||
and state_thermal_configuration
|
||||
in OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE
|
||||
):
|
||||
return OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE[
|
||||
state_thermal_configuration
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def device_hvac_mode(self) -> HVACMode | None:
|
||||
"""ZoneControlZone device has a single possible mode."""
|
||||
|
||||
return (
|
||||
None
|
||||
if self.thermal_configuration is None
|
||||
else self.thermal_configuration[0]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -61,21 +164,37 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
)
|
||||
|
||||
@property
|
||||
def zone_control_hvac_mode(self) -> HVACMode:
|
||||
def zone_control_hvac_action(self) -> HVACAction:
|
||||
"""Return hvac operation ie. heat, cool, dry, off mode."""
|
||||
|
||||
if (
|
||||
self.zone_control_device is not None
|
||||
and (
|
||||
state := self.zone_control_device.states[
|
||||
if self.zone_control_executor is not None and (
|
||||
(
|
||||
state := self.zone_control_executor.select_state(
|
||||
OverkizState.IO_PASS_APC_OPERATING_MODE
|
||||
]
|
||||
)
|
||||
)
|
||||
is not None
|
||||
and (value := state.value_as_str) is not None
|
||||
):
|
||||
return OVERKIZ_TO_HVAC_MODE[value]
|
||||
return HVACMode.OFF
|
||||
return OVERKIZ_TO_HVAC_ACTION[cast(str, state)]
|
||||
|
||||
return HVACAction.OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current running hvac operation."""
|
||||
|
||||
# When ZoneControl action is heating/cooling but Zone is stopped, means the zone is idle.
|
||||
if (
|
||||
hvac_action := self.zone_control_hvac_action
|
||||
) in HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE and cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE[hvac_action]
|
||||
),
|
||||
) == OverkizCommandParam.STOP:
|
||||
return HVACAction.IDLE
|
||||
|
||||
return hvac_action
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
@@ -84,30 +203,32 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().hvac_mode
|
||||
|
||||
zone_control_hvac_mode = self.zone_control_hvac_mode
|
||||
if (device_hvac_mode := self.device_hvac_mode) is None:
|
||||
return HVACMode.OFF
|
||||
|
||||
# Should be same, because either thermostat or this integration change both.
|
||||
on_off_state = cast(
|
||||
cooling_is_off = cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_COOLING_ON_OFF
|
||||
if zone_control_hvac_mode == HVACMode.COOL
|
||||
else OverkizState.CORE_HEATING_ON_OFF
|
||||
),
|
||||
)
|
||||
self.executor.select_state(OverkizState.CORE_COOLING_ON_OFF),
|
||||
) in (OverkizCommandParam.OFF, None)
|
||||
|
||||
heating_is_off = cast(
|
||||
str,
|
||||
self.executor.select_state(OverkizState.CORE_HEATING_ON_OFF),
|
||||
) in (OverkizCommandParam.OFF, None)
|
||||
|
||||
# Device is Stopped, it means the air flux is flowing but its venting door is closed.
|
||||
if on_off_state == OverkizCommandParam.OFF:
|
||||
hvac_mode = HVACMode.OFF
|
||||
else:
|
||||
hvac_mode = zone_control_hvac_mode
|
||||
if (
|
||||
(device_hvac_mode == HVACMode.COOL and cooling_is_off)
|
||||
or (device_hvac_mode == HVACMode.HEAT and heating_is_off)
|
||||
or (
|
||||
device_hvac_mode == HVACMode.HEAT_COOL
|
||||
and cooling_is_off
|
||||
and heating_is_off
|
||||
)
|
||||
):
|
||||
return HVACMode.OFF
|
||||
|
||||
# It helps keep it consistent with the Zone Control, within the interface.
|
||||
if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]:
|
||||
self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF]
|
||||
self.async_write_ha_state()
|
||||
|
||||
return hvac_mode
|
||||
return device_hvac_mode
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
@@ -118,46 +239,49 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
# They are mainly managed by the Zone Control device
|
||||
# However, it make sense to map the OFF Mode to the Overkiz STOP Preset
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_ON_OFF,
|
||||
OverkizCommandParam.OFF,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_ON_OFF,
|
||||
OverkizCommandParam.OFF,
|
||||
)
|
||||
else:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_ON_OFF,
|
||||
OverkizCommandParam.ON,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_ON_OFF,
|
||||
OverkizCommandParam.ON,
|
||||
)
|
||||
on_off_target_command_param = (
|
||||
OverkizCommandParam.OFF
|
||||
if hvac_mode == HVACMode.OFF
|
||||
else OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_ON_OFF,
|
||||
on_off_target_command_param,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_ON_OFF,
|
||||
on_off_target_command_param,
|
||||
)
|
||||
|
||||
await self.async_refresh_modes()
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., schedule, manual."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().preset_mode
|
||||
|
||||
mode = OVERKIZ_MODE_TO_PRESET_MODES[
|
||||
cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
OverkizState.IO_PASS_APC_COOLING_MODE
|
||||
if self.zone_control_hvac_mode == HVACMode.COOL
|
||||
else OverkizState.IO_PASS_APC_HEATING_MODE
|
||||
),
|
||||
if (
|
||||
self.zone_control_hvac_action in HVAC_ACTION_TO_OVERKIZ_MODE_STATE
|
||||
and (
|
||||
mode_state := HVAC_ACTION_TO_OVERKIZ_MODE_STATE[
|
||||
self.zone_control_hvac_action
|
||||
]
|
||||
)
|
||||
]
|
||||
and (
|
||||
(
|
||||
mode := OVERKIZ_MODE_TO_PRESET_MODES[
|
||||
cast(str, self.executor.select_state(mode_state))
|
||||
]
|
||||
)
|
||||
is not None
|
||||
)
|
||||
):
|
||||
return mode
|
||||
|
||||
return mode if mode is not None else PRESET_NONE
|
||||
return PRESET_NONE
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
@@ -178,13 +302,18 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
await self.async_refresh_modes()
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return hvac target temperature."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().target_temperature
|
||||
|
||||
if self.zone_control_hvac_mode == HVACMode.COOL:
|
||||
device_hvac_mode = self.device_hvac_mode
|
||||
|
||||
if device_hvac_mode == HVACMode.HEAT_COOL:
|
||||
return None
|
||||
|
||||
if device_hvac_mode == HVACMode.COOL:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
@@ -192,7 +321,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
),
|
||||
)
|
||||
|
||||
if self.zone_control_hvac_mode == HVACMode.HEAT:
|
||||
if device_hvac_mode == HVACMode.HEAT:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
@@ -204,32 +333,73 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE)
|
||||
)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach (cooling)."""
|
||||
|
||||
if self.device_hvac_mode != HVACMode.HEAT_COOL:
|
||||
return None
|
||||
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_COOLING_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach (heating)."""
|
||||
|
||||
if self.device_hvac_mode != HVACMode.HEAT_COOL:
|
||||
return None
|
||||
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_HEATING_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new temperature."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return await super().async_set_temperature(**kwargs)
|
||||
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
hvac_mode = self.hvac_mode
|
||||
|
||||
if hvac_mode == HVACMode.HEAT_COOL:
|
||||
if target_temp_low is not None:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
|
||||
target_temp_low,
|
||||
)
|
||||
|
||||
if target_temp_high is not None:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
|
||||
target_temp_high,
|
||||
)
|
||||
|
||||
elif target_temperature is not None:
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
|
||||
target_temperature,
|
||||
)
|
||||
|
||||
elif hvac_mode == HVACMode.COOL:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
|
||||
target_temperature,
|
||||
)
|
||||
|
||||
# Change both (heating/cooling) temperature is a good way to have consistency
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
|
||||
temperature,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
|
||||
temperature,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DEROGATION_ON_OFF_STATE,
|
||||
OverkizCommandParam.OFF,
|
||||
OverkizCommandParam.ON,
|
||||
)
|
||||
|
||||
# Target temperature may take up to 1 minute to get refreshed.
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.REFRESH_TARGET_TEMPERATURE
|
||||
)
|
||||
await self.async_refresh_modes()
|
||||
|
||||
async def async_refresh_modes(self) -> None:
|
||||
"""Refresh the device modes to have new states."""
|
||||
@@ -256,3 +426,51 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.REFRESH_TARGET_TEMPERATURE
|
||||
)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return Minimum Temperature for AC of this group."""
|
||||
|
||||
device_hvac_mode = self.device_hvac_mode
|
||||
|
||||
if device_hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL):
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MINIMUM_HEATING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
if device_hvac_mode == HVACMode.COOL:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MINIMUM_COOLING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return Max Temperature for AC of this group."""
|
||||
|
||||
device_hvac_mode = self.device_hvac_mode
|
||||
|
||||
if device_hvac_mode == HVACMode.HEAT:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
if device_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL):
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MAXIMUM_COOLING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
return super().max_temp
|
||||
|
||||
+6
-1
@@ -298,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
OverkizState.OVP_FAN_SPEED,
|
||||
OverkizCommandParam.AUTO,
|
||||
)
|
||||
# Sanitize fan mode: Overkiz is sometimes providing a state that
|
||||
# cannot be used as a command. Convert it to HA space and back to Overkiz
|
||||
if fan_mode not in FAN_MODES_TO_OVERKIZ.values():
|
||||
fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]]
|
||||
|
||||
hvac_mode = self._control_backfill(
|
||||
hvac_mode,
|
||||
OverkizState.OVP_MODE_CHANGE,
|
||||
@@ -357,5 +362,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
]
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.GLOBAL_CONTROL, command_data
|
||||
OverkizCommand.GLOBAL_CONTROL, *command_data
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.13.8"],
|
||||
"requirements": ["pyoverkiz==1.13.9"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -97,6 +97,28 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [
|
||||
max_value_state_name=OverkizState.CORE_MAXIMAL_SHOWER_MANUAL_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
OverkizNumberDescription(
|
||||
key=OverkizState.CORE_TARGET_DWH_TEMPERATURE,
|
||||
name="Target temperature",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
command=OverkizCommand.SET_TARGET_DHW_TEMPERATURE,
|
||||
native_min_value=50,
|
||||
native_max_value=65,
|
||||
min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
OverkizNumberDescription(
|
||||
key=OverkizState.CORE_WATER_TARGET_TEMPERATURE,
|
||||
name="Water target temperature",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
command=OverkizCommand.SET_WATER_TARGET_TEMPERATURE,
|
||||
native_min_value=50,
|
||||
native_max_value=65,
|
||||
min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
# SomfyHeatingTemperatureInterface
|
||||
OverkizNumberDescription(
|
||||
key=OverkizState.CORE_ECO_ROOM_TEMPERATURE,
|
||||
|
||||
@@ -399,6 +399,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_BOTTOM_TANK_WATER_TEMPERATURE,
|
||||
name="Bottom tank water temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONTROL_WATER_TARGET_TEMPERATURE,
|
||||
name="Control water target temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
# Cover
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_TARGET_CLOSURE,
|
||||
|
||||
@@ -42,7 +42,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
INVALID_TOKEN_MESSAGE,
|
||||
PLATFORMS,
|
||||
PLATFORMS_COMPLETED,
|
||||
PLEX_SERVER_CONFIG,
|
||||
PLEX_UPDATE_LIBRARY_SIGNAL,
|
||||
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
||||
@@ -94,18 +93,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
gdm.scan(scan_for_clients=True)
|
||||
|
||||
debouncer = Debouncer[None](
|
||||
hass,
|
||||
_LOGGER,
|
||||
cooldown=10,
|
||||
immediate=True,
|
||||
function=gdm_scan,
|
||||
hass, _LOGGER, cooldown=10, immediate=True, function=gdm_scan, background=True
|
||||
).async_call
|
||||
|
||||
hass_data = PlexData(
|
||||
servers={},
|
||||
dispatchers={},
|
||||
websockets={},
|
||||
platforms_completed={},
|
||||
gdm_scanner=gdm,
|
||||
gdm_debouncer=debouncer,
|
||||
)
|
||||
@@ -180,7 +174,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
server_id = plex_server.machine_identifier
|
||||
hass_data = get_plex_data(hass)
|
||||
hass_data[SERVERS][server_id] = plex_server
|
||||
hass_data[PLATFORMS_COMPLETED][server_id] = set()
|
||||
|
||||
entry.add_update_listener(async_options_updated)
|
||||
|
||||
@@ -233,11 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
hass_data[WEBSOCKETS][server_id] = websocket
|
||||
|
||||
def start_websocket_session(platform):
|
||||
hass_data[PLATFORMS_COMPLETED][server_id].add(platform)
|
||||
if hass_data[PLATFORMS_COMPLETED][server_id] == PLATFORMS:
|
||||
hass.loop.create_task(websocket.listen())
|
||||
|
||||
def close_websocket_session(_):
|
||||
websocket.close()
|
||||
|
||||
@@ -248,8 +236,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
for platform in PLATFORMS:
|
||||
start_websocket_session(platform)
|
||||
entry.async_create_background_task(
|
||||
hass, websocket.listen(), f"plex websocket listener {entry.entry_id}"
|
||||
)
|
||||
|
||||
async_cleanup_plex_devices(hass, entry)
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ GDM_SCANNER: Final = "gdm_scanner"
|
||||
PLATFORMS = frozenset(
|
||||
[Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE]
|
||||
)
|
||||
PLATFORMS_COMPLETED: Final = "platforms_completed"
|
||||
PLAYER_SOURCE = "player_source"
|
||||
SERVERS: Final = "servers"
|
||||
WEBSOCKETS: Final = "websockets"
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypedDict
|
||||
from plexapi.gdm import GDM
|
||||
from plexwebsocket import PlexWebsocket
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
|
||||
from .const import DOMAIN, SERVERS
|
||||
@@ -23,7 +22,6 @@ class PlexData(TypedDict):
|
||||
servers: dict[str, PlexServer]
|
||||
dispatchers: dict[str, list[CALLBACK_TYPE]]
|
||||
websockets: dict[str, PlexWebsocket]
|
||||
platforms_completed: dict[str, set[Platform]]
|
||||
gdm_scanner: GDM
|
||||
gdm_debouncer: Callable[[], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["plexapi", "plexwebsocket"],
|
||||
"requirements": [
|
||||
"PlexAPI==4.15.10",
|
||||
"PlexAPI==4.15.11",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.14"
|
||||
],
|
||||
|
||||
@@ -97,6 +97,7 @@ class PlexServer:
|
||||
cooldown=DEBOUNCE_TIMEOUT,
|
||||
immediate=True,
|
||||
function=self._async_update_platforms,
|
||||
background=True,
|
||||
).async_call
|
||||
self.thumbnail_cache = {}
|
||||
|
||||
|
||||
@@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase):
|
||||
"start_ts",
|
||||
unique=True,
|
||||
),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_STATISTICS
|
||||
|
||||
@@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase):
|
||||
"start_ts",
|
||||
unique=True,
|
||||
),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_STATISTICS_SHORT_TERM
|
||||
|
||||
@@ -760,7 +762,10 @@ class StatisticsMeta(Base):
|
||||
class RecorderRuns(Base):
|
||||
"""Representation of recorder run."""
|
||||
|
||||
__table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
|
||||
__table_args__ = (
|
||||
Index("ix_recorder_runs_start_end", "start", "end"),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_RECORDER_RUNS
|
||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
@@ -789,6 +794,7 @@ class MigrationChanges(Base):
|
||||
"""Representation of migration changes."""
|
||||
|
||||
__tablename__ = TABLE_MIGRATION_CHANGES
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
migration_id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||
version: Mapped[int] = mapped_column(SmallInteger)
|
||||
@@ -798,6 +804,8 @@ class SchemaChanges(Base):
|
||||
"""Representation of schema version changes."""
|
||||
|
||||
__tablename__ = TABLE_SCHEMA_CHANGES
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
schema_version: Mapped[int | None] = mapped_column(Integer)
|
||||
changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
@@ -816,6 +824,8 @@ class StatisticsRuns(Base):
|
||||
"""Representation of statistics run."""
|
||||
|
||||
__tablename__ = TABLE_STATISTICS_RUNS
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True)
|
||||
|
||||
|
||||
@@ -125,16 +125,16 @@ class RenaultVehicleProxy:
|
||||
coordinator = self.coordinators[key]
|
||||
if coordinator.not_supported:
|
||||
# Remove endpoint as it is not supported for this vehicle.
|
||||
LOGGER.info(
|
||||
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
|
||||
LOGGER.warning(
|
||||
"Ignoring endpoint %s as it is not supported: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
)
|
||||
del self.coordinators[key]
|
||||
elif coordinator.access_denied:
|
||||
# Remove endpoint as it is denied for this vehicle.
|
||||
LOGGER.info(
|
||||
"Ignoring endpoint %s as it is denied for this vehicle: %s",
|
||||
LOGGER.warning(
|
||||
"Ignoring endpoint %s as it is denied: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
)
|
||||
|
||||
@@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
"""Initialize ReolinkVODMediaSource."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.data: dict[str, ReolinkData] = hass.data[DOMAIN]
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
@@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
_, config_entry_id, channel_str, stream_res, filename = identifier
|
||||
channel = int(channel_str)
|
||||
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
vod_type = VodRequestType.RTMP
|
||||
if host.api.is_nvr:
|
||||
@@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
continue
|
||||
channels: list[str] = []
|
||||
host = self.data[config_entry.entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry.entry_id].host
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_reg, config_entry.entry_id
|
||||
)
|
||||
@@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int
|
||||
) -> BrowseMediaSource:
|
||||
"""Allow the user to select the high or low playback resolution, (low loads faster)."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
main_enc = await host.api.get_encoding(channel, "main")
|
||||
if main_enc == "h265":
|
||||
@@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int, stream: str
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all days on which recordings are available for a reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
# We want today of the camera, not necessarily today of the server
|
||||
now = host.api.time() or await host.api.async_get_time()
|
||||
@@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
day: int,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all recording files on a specific day of a Reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
start = dt.datetime(year, month, day, hour=0, minute=0, second=0)
|
||||
end = dt.datetime(year, month, day, hour=23, minute=59, second=59)
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ring",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"requirements": ["ring-doorbell[listen]==0.8.7"]
|
||||
"requirements": ["ring-doorbell[listen]==0.8.9"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/romy",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["romy==0.0.7"],
|
||||
"requirements": ["romy==0.0.10"],
|
||||
"zeroconf": ["_aicu-http._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["paho_mqtt", "roombapy"],
|
||||
"requirements": ["roombapy==1.6.13"],
|
||||
"requirements": ["roombapy==1.8.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_amzn-alexa._tcp.local.",
|
||||
|
||||
@@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string,
|
||||
vol.Optional(CONF_NAME, default="Rova"): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.6.0",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.38.2"
|
||||
"async-upnp-client==0.38.3"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
|
||||
)
|
||||
|
||||
CONF_GEN = "gen"
|
||||
|
||||
SHELLY_PLUS_RGBW_CHANNELS = 4
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.light import (
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
@@ -34,12 +35,14 @@ from .const import (
|
||||
RGBW_MODELS,
|
||||
RPC_MIN_TRANSITION_TIME_SEC,
|
||||
SHBLB_1_RGB_EFFECTS,
|
||||
SHELLY_PLUS_RGBW_CHANNELS,
|
||||
STANDARD_RGB_EFFECTS,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
||||
from .utils import (
|
||||
async_remove_shelly_entity,
|
||||
async_remove_shelly_rpc_entities,
|
||||
brightness_to_percentage,
|
||||
get_device_entry_gen,
|
||||
get_rpc_key_ids,
|
||||
@@ -118,14 +121,28 @@ def async_setup_rpc_entry(
|
||||
return
|
||||
|
||||
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
|
||||
# Light mode remove RGB & RGBW entities, add light entities
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
|
||||
return
|
||||
|
||||
light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)]
|
||||
|
||||
if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
|
||||
# RGB mode remove light & RGBW entities, add RGB entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
|
||||
return
|
||||
|
||||
if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
|
||||
# RGBW mode remove light & RGB entities, add RGBW entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
|
||||
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize update entity."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
self._ota_in_progress: bool = False
|
||||
self._ota_in_progress: bool | int = False
|
||||
self._attr_release_url = get_release_url(
|
||||
coordinator.device.gen, coordinator.model, description.beta
|
||||
)
|
||||
@@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
@callback
|
||||
def _ota_progress_callback(self, event: dict[str, Any]) -> None:
|
||||
"""Handle device OTA progress."""
|
||||
if self._ota_in_progress:
|
||||
if self.in_progress is not False:
|
||||
event_type = event["event"]
|
||||
if event_type == OTA_BEGIN:
|
||||
self._attr_in_progress = 0
|
||||
self._ota_in_progress = 0
|
||||
elif event_type == OTA_PROGRESS:
|
||||
self._attr_in_progress = event["progress_percent"]
|
||||
self._ota_in_progress = event["progress_percent"]
|
||||
elif event_type in (OTA_ERROR, OTA_SUCCESS):
|
||||
self._attr_in_progress = False
|
||||
self._ota_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
|
||||
return self.installed_version
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool | int:
|
||||
"""Update installation in progress."""
|
||||
return self._ota_in_progress
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
@@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
await self.coordinator.async_shutdown_device_and_start_reauth()
|
||||
else:
|
||||
self._ota_in_progress = True
|
||||
LOGGER.debug("OTA update call successful")
|
||||
LOGGER.info("OTA update call for %s successful", self.coordinator.name)
|
||||
|
||||
|
||||
class RpcSleepingUpdateEntity(
|
||||
|
||||
@@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None:
|
||||
await device.shutdown()
|
||||
if isinstance(device, BlockDevice):
|
||||
device.shutdown()
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_shelly_rpc_entities(
|
||||
hass: HomeAssistant, domain: str, mac: str, keys: list[str]
|
||||
) -> None:
|
||||
"""Remove RPC based Shelly entity."""
|
||||
entity_reg = er_async_get(hass)
|
||||
for key in keys:
|
||||
if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"):
|
||||
LOGGER.debug("Removing entity: %s", entity_id)
|
||||
entity_reg.async_remove(entity_id)
|
||||
|
||||
@@ -270,7 +270,7 @@ class SnmpData:
|
||||
"SNMP OID %s received type=%s and data %s",
|
||||
self._baseoid,
|
||||
type(value),
|
||||
bytes(value),
|
||||
value,
|
||||
)
|
||||
if isinstance(value, NoSuchObject):
|
||||
_LOGGER.error(
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
import urllib.parse
|
||||
|
||||
from soco.data_structures import DidlObject
|
||||
from soco.ms_data_structures import MusicServiceItem
|
||||
@@ -60,12 +61,14 @@ def get_thumbnail_url_full(
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
)
|
||||
return getattr(item, "album_art_uri", None)
|
||||
return urllib.parse.unquote(getattr(item, "album_art_uri", ""))
|
||||
|
||||
return get_browse_image_url(
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
media_image_id,
|
||||
return urllib.parse.unquote(
|
||||
get_browse_image_url(
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
media_image_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -166,6 +169,7 @@ def build_item_response(
|
||||
payload["idstring"] = "A:ALBUMARTIST/" + "/".join(
|
||||
payload["idstring"].split("/")[2:]
|
||||
)
|
||||
payload["idstring"] = urllib.parse.unquote(payload["idstring"])
|
||||
|
||||
try:
|
||||
search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]]
|
||||
@@ -201,7 +205,7 @@ def build_item_response(
|
||||
|
||||
if not title:
|
||||
try:
|
||||
title = payload["idstring"].split("/")[1]
|
||||
title = urllib.parse.unquote(payload["idstring"].split("/")[1])
|
||||
except IndexError:
|
||||
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
|
||||
|
||||
@@ -493,10 +497,24 @@ def get_media(
|
||||
"""Fetch media/album."""
|
||||
search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type)
|
||||
|
||||
if search_type == "playlists":
|
||||
# Format is S:TITLE or S:ITEM_ID
|
||||
splits = item_id.split(":")
|
||||
title = splits[1] if len(splits) > 1 else None
|
||||
playlist = next(
|
||||
(
|
||||
p
|
||||
for p in media_library.get_playlists()
|
||||
if (item_id == p.item_id or title == p.title)
|
||||
),
|
||||
None,
|
||||
)
|
||||
return playlist
|
||||
|
||||
if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM:
|
||||
item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:])
|
||||
|
||||
search_term = item_id.split("/")[-1]
|
||||
search_term = urllib.parse.unquote(item_id.split("/")[-1])
|
||||
matches = media_library.get_music_library_information(
|
||||
search_type, search_term=search_term, full_album_art_uri=True
|
||||
)
|
||||
|
||||
@@ -626,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
soco.play_uri(media_id, force_radio=is_radio)
|
||||
elif media_type == MediaType.PLAYLIST:
|
||||
if media_id.startswith("S:"):
|
||||
item = media_browser.get_media(self.media.library, media_id, media_type)
|
||||
soco.play_uri(item.get_uri())
|
||||
return
|
||||
try:
|
||||
playlist = media_browser.get_media(
|
||||
self.media.library, media_id, media_type
|
||||
)
|
||||
else:
|
||||
playlists = soco.get_sonos_playlists(complete_result=True)
|
||||
playlist = next(p for p in playlists if p.title == media_id)
|
||||
except StopIteration:
|
||||
playlist = next((p for p in playlists if p.title == media_id), None)
|
||||
if not playlist:
|
||||
_LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
|
||||
else:
|
||||
soco.clear_queue()
|
||||
|
||||
@@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
partial(speedtest.Speedtest, secure=True)
|
||||
)
|
||||
coordinator = SpeedTestDataCoordinator(hass, config_entry, api)
|
||||
await hass.async_add_executor_job(coordinator.update_servers)
|
||||
except speedtest.SpeedtestException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
# Don't start a speedtest during startup
|
||||
async_at_started(hass, _async_finish_startup)
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["async-upnp-client==0.38.2"]
|
||||
"requirements": ["async-upnp-client==0.38.3"]
|
||||
}
|
||||
|
||||
@@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
async def _async_update_data(self) -> StarlinkData:
|
||||
async with asyncio.timeout(4):
|
||||
try:
|
||||
status, location, sleep = await asyncio.gather(
|
||||
self.hass.async_add_executor_job(status_data, self.channel_context),
|
||||
self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
),
|
||||
self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
),
|
||||
status = await self.hass.async_add_executor_job(
|
||||
status_data, self.channel_context
|
||||
)
|
||||
location = await self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
)
|
||||
sleep = await self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
)
|
||||
return StarlinkData(location, sleep, *status)
|
||||
except GrpcError as exc:
|
||||
|
||||
@@ -10,6 +10,7 @@ import math
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity):
|
||||
def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
|
||||
hour = math.floor(utc_minutes / 60)
|
||||
minute = utc_minutes % 60
|
||||
utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
try:
|
||||
utc = datetime.now(UTC).replace(
|
||||
hour=hour, minute=minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
return utc.astimezone(timezone).time()
|
||||
|
||||
|
||||
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
try:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
utc_time = zoned_time.astimezone(UTC).time()
|
||||
return (utc_time.hour * 60) + utc_time.minute
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if (
|
||||
SynoSurveillanceStation.INFO_API_KEY in available_apis
|
||||
and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis
|
||||
and api.surveillance_station is not None
|
||||
):
|
||||
coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api)
|
||||
await coordinator_switches.async_config_entry_first_refresh()
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reboot": "mdi:reboot",
|
||||
"reboot": "mdi:restart",
|
||||
"shutdown": "mdi:power"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["systembridgeconnector"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["systembridgeconnector==4.0.3"],
|
||||
"requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"],
|
||||
"zeroconf": ["_system-bridge._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -166,7 +166,6 @@ class LogEntry:
|
||||
"level",
|
||||
"message",
|
||||
"exception",
|
||||
"extracted_tb",
|
||||
"root_cause",
|
||||
"source",
|
||||
"count",
|
||||
@@ -200,7 +199,6 @@ class LogEntry:
|
||||
else:
|
||||
self.source = (record.pathname, record.lineno)
|
||||
self.count = 1
|
||||
self.extracted_tb = extracted_tb
|
||||
self.key = (self.name, self.source, self.root_cause)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
|
||||
@@ -34,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
|
||||
is_on=lambda x: x == TessieState.ONLINE,
|
||||
),
|
||||
TessieBinarySensorEntityDescription(
|
||||
key="charge_state_battery_heater_on",
|
||||
key="climate_state_battery_heater",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
"state": {
|
||||
"name": "Status"
|
||||
},
|
||||
"charge_state_battery_heater_on": {
|
||||
"climate_state_battery_heater": {
|
||||
"name": "Battery heater"
|
||||
},
|
||||
"charge_state_charge_enable_request": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"pause": "mdi:pause",
|
||||
"cancel": "mdi:cancel",
|
||||
"finish": "mdi:check",
|
||||
|
||||
@@ -107,6 +107,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"inaccurate_position",
|
||||
"not_reporting",
|
||||
"operational",
|
||||
"system_shutdown_user",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"tracker_state": {
|
||||
"name": "Tracker state",
|
||||
"state": {
|
||||
"inaccurate_position": "Inaccurate position",
|
||||
"not_reporting": "Not reporting",
|
||||
"operational": "Operational",
|
||||
"system_shutdown_user": "System shutdown user",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==73"],
|
||||
"requirements": ["aiounifi==74"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2023.12.0"],
|
||||
"requirements": ["velbus-aio==2024.4.0"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["weatherflow4py==0.2.17"]
|
||||
"requirements": ["weatherflow4py==0.2.20"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Diagnostics support for Whirlpool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import WhirlpoolData
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT = {
|
||||
"SERIAL_NUMBER",
|
||||
"macaddress",
|
||||
"username",
|
||||
"password",
|
||||
"token",
|
||||
"unique_id",
|
||||
"SAID",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
diagnostics_data = {
|
||||
"Washer_dryers": {
|
||||
wd["NAME"]: dict(wd.items())
|
||||
for wd in whirlpool.appliances_manager.washer_dryers
|
||||
},
|
||||
"aircons": {
|
||||
ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons
|
||||
},
|
||||
"ovens": {
|
||||
oven["NAME"]: dict(oven.items())
|
||||
for oven in whirlpool.appliances_manager.ovens
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||
"appliances": async_redact_data(diagnostics_data, TO_REDACT),
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["whirlpool"],
|
||||
"requirements": ["whirlpool-sixth-sense==0.18.6"]
|
||||
"requirements": ["whirlpool-sixth-sense==0.18.7"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from holidays import HolidayBase, country_holidays
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -13,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
||||
from .const import CONF_PROVINCE, DOMAIN, PLATFORMS
|
||||
|
||||
|
||||
def _validate_country_and_province(
|
||||
async def _async_validate_country_and_province(
|
||||
hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None
|
||||
) -> None:
|
||||
"""Validate country and province."""
|
||||
@@ -21,7 +23,7 @@ def _validate_country_and_province(
|
||||
if not country:
|
||||
return
|
||||
try:
|
||||
country_holidays(country)
|
||||
await hass.async_add_executor_job(country_holidays, country)
|
||||
except NotImplementedError as ex:
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -39,7 +41,9 @@ def _validate_country_and_province(
|
||||
if not province:
|
||||
return
|
||||
try:
|
||||
country_holidays(country, subdiv=province)
|
||||
await hass.async_add_executor_job(
|
||||
partial(country_holidays, country, subdiv=province)
|
||||
)
|
||||
except NotImplementedError as ex:
|
||||
async_create_issue(
|
||||
hass,
|
||||
@@ -66,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
country: str | None = entry.options.get(CONF_COUNTRY)
|
||||
province: str | None = entry.options.get(CONF_PROVINCE)
|
||||
|
||||
_validate_country_and_province(hass, entry, country, province)
|
||||
await _async_validate_country_and_province(hass, entry, country, province)
|
||||
|
||||
if country and CONF_LANGUAGE not in entry.options:
|
||||
cls: HolidayBase = country_holidays(country, subdiv=province)
|
||||
cls: HolidayBase = await hass.async_add_executor_job(
|
||||
partial(country_holidays, country, subdiv=province)
|
||||
)
|
||||
default_language = cls.default_language
|
||||
new_options = entry.options.copy()
|
||||
new_options[CONF_LANGUAGE] = default_language
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.45"]
|
||||
"requirements": ["holidays==0.46"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"switch_set_wifi_led_off": "mdi:wifi-off",
|
||||
"switch_set_power_price": "mdi:currency-usd",
|
||||
"switch_set_power_mode": "mdi:power",
|
||||
"vacuum_remote_control_start": "mdi:start",
|
||||
"vacuum_remote_control_start": "mdi:play",
|
||||
"vacuum_remote_control_stop": "mdi:stop",
|
||||
"vacuum_remote_control_move": "mdi:remote",
|
||||
"vacuum_remote_control_move_step": "mdi:remote",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client", "yeelight"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"],
|
||||
"requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_miio._udp.local.",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.131.0"]
|
||||
"requirements": ["zeroconf==0.132.0"]
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
zha_data = get_zha_data(hass)
|
||||
|
||||
if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True):
|
||||
setup_quirks(
|
||||
custom_quirks_path=zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH)
|
||||
await hass.async_add_import_executor_job(
|
||||
setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH)
|
||||
)
|
||||
|
||||
# Load and cache device trigger information early
|
||||
|
||||
@@ -553,6 +553,13 @@ class OtaClientClusterHandler(ClientClusterHandler):
|
||||
Ota.AttributeDefs.current_file_version.name: True,
|
||||
}
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None:
|
||||
"""Handle an attribute updated on this cluster."""
|
||||
# We intentionally avoid the `ClientClusterHandler` attribute update handler:
|
||||
# it emits a logbook event on every update, which pollutes the logbook
|
||||
ClusterHandler.attribute_updated(self, attrid, value, timestamp)
|
||||
|
||||
@property
|
||||
def current_file_version(self) -> int | None:
|
||||
"""Return cached value of current_file_version attribute."""
|
||||
|
||||
@@ -130,14 +130,9 @@ class ZHAFirmwareUpdateEntity(
|
||||
def _get_cluster_version(self) -> str | None:
|
||||
"""Synchronize current file version with the cluster."""
|
||||
|
||||
device = self._ota_cluster_handler._endpoint.device # pylint: disable=protected-access
|
||||
|
||||
if self._ota_cluster_handler.current_file_version is not None:
|
||||
return f"0x{self._ota_cluster_handler.current_file_version:08x}"
|
||||
|
||||
if device.sw_version is not None:
|
||||
return device.sw_version
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user