mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 16:39:02 +02:00
Compare commits
174 Commits
2025.8.2
...
revert-149
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ff488519 | ||
|
|
119d0a0170 | ||
|
|
69faf38e86 | ||
|
|
d0ef1a1a8b | ||
|
|
8f328810bf | ||
|
|
4f1b75e3b4 | ||
|
|
445a7fc749 | ||
|
|
977c0797aa | ||
|
|
a24f027923 | ||
|
|
7b45798e30 | ||
|
|
2b0cda0ad1 | ||
|
|
12dca4b1bf | ||
|
|
8c509b11b2 | ||
|
|
991c9008bd | ||
|
|
fe95f6e1c5 | ||
|
|
37510aa316 | ||
|
|
4e40e9bf74 | ||
|
|
70c9b1f095 | ||
|
|
f714388130 | ||
|
|
ffb2a693f4 | ||
|
|
9d8e253ad3 | ||
|
|
31631cc882 | ||
|
|
3a64357201 | ||
|
|
20fdec9e9c | ||
|
|
064a63fe1f | ||
|
|
803654223a | ||
|
|
a6148b50cf | ||
|
|
02a3c5be14 | ||
|
|
08ea640629 | ||
|
|
7dd761c9c3 | ||
|
|
6b827dfc33 | ||
|
|
67c19087dd | ||
|
|
55c7c2f730 | ||
|
|
afee936c3d | ||
|
|
ed2ced6c36 | ||
|
|
4c5cf028d7 | ||
|
|
68faa897ad | ||
|
|
53c9c42148 | ||
|
|
d48cc03be7 | ||
|
|
28236aa023 | ||
|
|
bfae07135a | ||
|
|
99d580e371 | ||
|
|
4d53450cbf | ||
|
|
1fbce01e26 | ||
|
|
a9621ac811 | ||
|
|
94f2118b19 | ||
|
|
73ca6b4900 | ||
|
|
31e647b5b0 | ||
|
|
fac5b2c09c | ||
|
|
ae48179e95 | ||
|
|
88c9d5dbe3 | ||
|
|
b76f47cd9f | ||
|
|
822e1ffc8d | ||
|
|
1632e0aef6 | ||
|
|
e2bc73f153 | ||
|
|
46cfdddc80 | ||
|
|
0bdf6757c4 | ||
|
|
312e590360 | ||
|
|
7a6aaf667b | ||
|
|
33eaca24d6 | ||
|
|
3d27d501b1 | ||
|
|
39b651e075 | ||
|
|
a962777a2e | ||
|
|
594ce8f266 | ||
|
|
9f867f268c | ||
|
|
9edd242734 | ||
|
|
93e11aa8bc | ||
|
|
c2b298283e | ||
|
|
106c086e8b | ||
|
|
cbf4130bff | ||
|
|
afffe0b08b | ||
|
|
c1ccfee7cc | ||
|
|
8d8383e1c1 | ||
|
|
f350a1a1fa | ||
|
|
fe2bd8d09e | ||
|
|
cf14226b02 | ||
|
|
bd3fe1d4ad | ||
|
|
377ca04be8 | ||
|
|
5837f55205 | ||
|
|
0766edb9c4 | ||
|
|
e62e3778f3 | ||
|
|
aa8e4c1c15 | ||
|
|
46ed8a73fc | ||
|
|
83f22497ae | ||
|
|
3dda1685dc | ||
|
|
6fa9d42401 | ||
|
|
1a54d566f8 | ||
|
|
1a9cae0f89 | ||
|
|
551dcaa169 | ||
|
|
5467db065b | ||
|
|
6a8d752e56 | ||
|
|
179a56628d | ||
|
|
b3f830773a | ||
|
|
084e06ec7d | ||
|
|
e0190afd3c | ||
|
|
b9e16d54c4 | ||
|
|
627785edc1 | ||
|
|
4318e29ce8 | ||
|
|
fea5c63bba | ||
|
|
b2349ac2bd | ||
|
|
08f7b708a4 | ||
|
|
1236801b7d | ||
|
|
72d9dbf39d | ||
|
|
755864f9f3 | ||
|
|
fa476d4e34 | ||
|
|
018197e41a | ||
|
|
7dd2b9e422 | ||
|
|
3e615fd373 | ||
|
|
c0bf167e10 | ||
|
|
45f6778ff4 | ||
|
|
bddd4d621a | ||
|
|
b0e75e9ee4 | ||
|
|
d45c03a795 | ||
|
|
8562c8d32f | ||
|
|
ae42d71123 | ||
|
|
9616c8cd7b | ||
|
|
9394546668 | ||
|
|
d43f21c2e2 | ||
|
|
8d68fee9f8 | ||
|
|
b4a4e218ec | ||
|
|
fb2d62d692 | ||
|
|
f538807d6e | ||
|
|
a08c3c9f44 | ||
|
|
506431c75f | ||
|
|
37579440e6 | ||
|
|
5ce2729dc2 | ||
|
|
b5e4ae4a53 | ||
|
|
3d4386ea6d | ||
|
|
9f1cec893e | ||
|
|
bc87140a6f | ||
|
|
d77a3fca83 | ||
|
|
924a86dfb6 | ||
|
|
0d7608f7c5 | ||
|
|
22e054f4cd | ||
|
|
8b53b26333 | ||
|
|
4d59e8cd80 | ||
|
|
61396d92a5 | ||
|
|
c72c600de4 | ||
|
|
b86b0c10bd | ||
|
|
eb222f6c5d | ||
|
|
4b5fe424ed | ||
|
|
61ca42e923 | ||
|
|
21c1427abf | ||
|
|
aa6b37bc7c | ||
|
|
bbc1466cfc | ||
|
|
21a9799060 | ||
|
|
f7d54b46ec | ||
|
|
6ad1b8dcb1 | ||
|
|
5f6b1212a3 | ||
|
|
58dc6a952e | ||
|
|
59d8df142d | ||
|
|
04fb86b4ba | ||
|
|
3d744f032f | ||
|
|
f7c8cdb3a7 | ||
|
|
3952544822 | ||
|
|
42101dd432 | ||
|
|
f7eacaa48d | ||
|
|
ad0db5c83a | ||
|
|
63216b77c2 | ||
|
|
7a55373b0b | ||
|
|
f9e7459901 | ||
|
|
94dc2e2ea3 | ||
|
|
2cf144fb25 | ||
|
|
f318766021 | ||
|
|
ec7fb140ac | ||
|
|
2706c7d67d | ||
|
|
b4e50902eb | ||
|
|
1ead01bc9a | ||
|
|
389a1251a1 | ||
|
|
8d27ca1e21 | ||
|
|
a76af50c10 | ||
|
|
09b91bd76a | ||
|
|
736d582d04 | ||
|
|
8114df4219 |
10
.github/workflows/builder.yml
vendored
10
.github/workflows/builder.yml
vendored
@@ -190,7 +190,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -330,14 +330,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -502,7 +502,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 4
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.8"
|
||||
HA_SHORT_VERSION: "2025.9"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@v1.2.3
|
||||
uses: actions/ai-inference@v1.2.7
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@v1.2.3
|
||||
uses: actions/ai-inference@v1.2.7
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
||||
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -159,7 +159,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -33,7 +33,10 @@ class AuthFlowContext(FlowContext, total=False):
|
||||
redirect_uri: str
|
||||
|
||||
|
||||
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
|
||||
class AuthFlowResult(FlowResult[AuthFlowContext, tuple[str, str]], total=False):
|
||||
"""Typed result dict for auth flow."""
|
||||
|
||||
result: Credentials # Only present if type is CREATE_ENTRY
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
|
||||
@@ -6,11 +6,11 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
KeyDataMissingError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
ConnectionSetupError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (ConnectionAuthenticationError, DataMissingError):
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except KeyDataMissingError:
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -6,10 +6,10 @@ import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (ConnectionAuthenticationError,) as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except (DataMissingError,) as err:
|
||||
except (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for airOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirOSConfigEntry
|
||||
|
||||
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||
TO_REDACT_AIROS = [
|
||||
"hostname", # Prevent leaking device naming
|
||||
"essid", # Network SSID
|
||||
"lat", # GPS latitude to prevent exposing location data.
|
||||
"lon", # GPS longitude to prevent exposing location data.
|
||||
*HW_REDACT,
|
||||
*IP_REDACT,
|
||||
]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.1"]
|
||||
"requirements": ["airos==0.2.4"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
|
||||
@@ -69,13 +69,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_mode",
|
||||
translation_key="wireless_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
||||
options=WIRELESS_MODE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
|
||||
@@ -43,13 +43,6 @@
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_mode": {
|
||||
"name": "Wireless mode",
|
||||
"state": {
|
||||
"ap_ptp": "Access point",
|
||||
"sta_ptp": "Station"
|
||||
}
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
|
||||
@@ -7,21 +7,18 @@ import logging
|
||||
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
airthings: Airthings,
|
||||
config_entry: AirthingsConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
||||
@@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"model": device.model,
|
||||
"sw_version": device.sw_version,
|
||||
"hw_version": device.hw_version,
|
||||
"has_suggested_area": device.suggested_area is not None,
|
||||
"has_configuration_url": device.configuration_url is not None,
|
||||
"via_device": None,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
|
||||
@@ -40,6 +40,9 @@ from .const import (
|
||||
SENSORS_CONNECTED_DEVICE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AsusWrtConfigEntry
|
||||
|
||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AsusWrtSensorDataHandler:
|
||||
"""Data handler for AsusWrt sensor."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
|
||||
) -> None:
|
||||
"""Initialize a AsusWrt sensor data handler."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._entry = entry
|
||||
self._connected_devices = 0
|
||||
|
||||
async def _get_connected_devices(self) -> dict[str, int]:
|
||||
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
|
||||
update_method=method,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=SCAN_INTERVAL if should_poll else None,
|
||||
config_entry=self._entry,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
@@ -321,7 +328,9 @@ class AsusWrtRouter:
|
||||
if self._sensors_data_handler:
|
||||
return
|
||||
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(
|
||||
self.hass, self._api, self._entry
|
||||
)
|
||||
self._sensors_data_handler.update_device_count(self._connected_devices)
|
||||
|
||||
sensors_types = await self._api.async_get_available_sensors()
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
|
||||
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ class LoginFlowBaseView(HomeAssistantView):
|
||||
result.pop("data")
|
||||
result.pop("context")
|
||||
|
||||
result_obj: Credentials = result.pop("result")
|
||||
result_obj = result.pop("result")
|
||||
|
||||
# Result can be None if credential was never linked to a user before.
|
||||
user = await hass.auth.async_get_user_by_credentials(result_obj)
|
||||
@@ -281,7 +281,8 @@ class LoginFlowBaseView(HomeAssistantView):
|
||||
)
|
||||
|
||||
process_success_login(request)
|
||||
result["result"] = self._store_result(client_id, result_obj)
|
||||
# We overwrite the Credentials object with the string code to retrieve it.
|
||||
result["result"] = self._store_result(client_id, result_obj) # type: ignore[typeddict-item]
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==64"],
|
||||
"requirements": ["axis==65"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -1119,7 +1119,7 @@ class BackupManager:
|
||||
)
|
||||
if unavailable_agents:
|
||||
LOGGER.warning(
|
||||
"Backup agents %s are not available, will backupp to %s",
|
||||
"Backup agents %s are not available, will backup to %s",
|
||||
unavailable_agents,
|
||||
available_agents,
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.44.2",
|
||||
"dbus-fast==2.44.3",
|
||||
"habluetooth==4.0.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
|
||||
device.hass,
|
||||
_LOGGER,
|
||||
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
|
||||
config_entry=device.config,
|
||||
update_method=self.async_update,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from bsblan import BSBLAN, BSBLANConfig, BSBLANError
|
||||
from bsblan import BSBLAN, BSBLANAuthError, BSBLANConfig, BSBLANError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -45,7 +46,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.username = user_input.get(CONF_USERNAME)
|
||||
self.password = user_input.get(CONF_PASSWORD)
|
||||
|
||||
return await self._validate_and_create()
|
||||
return await self._validate_and_create(user_input)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
@@ -128,14 +129,29 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.username = user_input.get(CONF_USERNAME)
|
||||
self.password = user_input.get(CONF_PASSWORD)
|
||||
|
||||
return await self._validate_and_create(is_discovery=True)
|
||||
return await self._validate_and_create(user_input, is_discovery=True)
|
||||
|
||||
async def _validate_and_create(
|
||||
self, is_discovery: bool = False
|
||||
self, user_input: dict[str, Any], is_discovery: bool = False
|
||||
) -> ConfigFlowResult:
|
||||
"""Validate device connection and create entry."""
|
||||
try:
|
||||
await self._get_bsblan_info(is_discovery=is_discovery)
|
||||
await self._get_bsblan_info()
|
||||
except BSBLANAuthError:
|
||||
if is_discovery:
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_PASSKEY): str,
|
||||
vol.Optional(CONF_USERNAME): str,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors={"base": "invalid_auth"},
|
||||
description_placeholders={"host": str(self.host)},
|
||||
)
|
||||
return self._show_setup_form({"base": "invalid_auth"}, user_input)
|
||||
except BSBLANError:
|
||||
if is_discovery:
|
||||
return self.async_show_form(
|
||||
@@ -154,18 +170,137 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self._async_create_entry()
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth flow."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth confirmation flow."""
|
||||
existing_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
assert existing_entry
|
||||
|
||||
if user_input is None:
|
||||
# Preserve existing values as defaults
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PASSKEY,
|
||||
default=existing_entry.data.get(
|
||||
CONF_PASSKEY, vol.UNDEFINED
|
||||
),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_USERNAME,
|
||||
default=existing_entry.data.get(
|
||||
CONF_USERNAME, vol.UNDEFINED
|
||||
),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_PASSWORD,
|
||||
default=vol.UNDEFINED,
|
||||
): str,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# Combine existing data with the user's new input for validation.
|
||||
# This correctly handles adding, changing, and clearing credentials.
|
||||
config_data = existing_entry.data.copy()
|
||||
config_data.update(user_input)
|
||||
|
||||
self.host = config_data[CONF_HOST]
|
||||
self.port = config_data[CONF_PORT]
|
||||
self.passkey = config_data.get(CONF_PASSKEY)
|
||||
self.username = config_data.get(CONF_USERNAME)
|
||||
self.password = config_data.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
|
||||
except BSBLANAuthError:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PASSKEY,
|
||||
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_USERNAME,
|
||||
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_PASSWORD,
|
||||
default=vol.UNDEFINED,
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors={"base": "invalid_auth"},
|
||||
)
|
||||
except BSBLANError:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PASSKEY,
|
||||
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_USERNAME,
|
||||
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_PASSWORD,
|
||||
default=vol.UNDEFINED,
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors={"base": "cannot_connect"},
|
||||
)
|
||||
|
||||
# Update only the fields that were provided by the user
|
||||
return self.async_update_reload_and_abort(
|
||||
existing_entry, data_updates=user_input, reason="reauth_successful"
|
||||
)
|
||||
|
||||
@callback
|
||||
def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult:
|
||||
def _show_setup_form(
|
||||
self, errors: dict | None = None, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the setup form to the user."""
|
||||
# Preserve user input if provided, otherwise use defaults
|
||||
defaults = user_input or {}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Optional(CONF_PASSKEY): str,
|
||||
vol.Optional(CONF_USERNAME): str,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
vol.Required(
|
||||
CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED)
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT)
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED)
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_USERNAME,
|
||||
default=defaults.get(CONF_USERNAME, vol.UNDEFINED),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_PASSWORD,
|
||||
default=defaults.get(CONF_PASSWORD, vol.UNDEFINED),
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
@@ -186,7 +321,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def _get_bsblan_info(
|
||||
self, raise_on_progress: bool = True, is_discovery: bool = False
|
||||
self,
|
||||
raise_on_progress: bool = True,
|
||||
is_reauth: bool = False,
|
||||
) -> None:
|
||||
"""Get device information from a BSBLAN device."""
|
||||
config = BSBLANConfig(
|
||||
@@ -209,11 +346,13 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
format_mac(self.mac), raise_on_progress=raise_on_progress
|
||||
)
|
||||
|
||||
# Always allow updating host/port for both user and discovery flows
|
||||
# This ensures connectivity is maintained when devices change IP addresses
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self.host,
|
||||
CONF_PORT: self.port,
|
||||
}
|
||||
)
|
||||
# Skip unique_id configuration check during reauth to prevent "already_configured" abort
|
||||
if not is_reauth:
|
||||
# Always allow updating host/port for both user and discovery flows
|
||||
# This ensures connectivity is maintained when devices change IP addresses
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self.host,
|
||||
CONF_PORT: self.port,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,11 +4,19 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from random import randint
|
||||
|
||||
from bsblan import BSBLAN, BSBLANConnectionError, HotWaterState, Sensor, State
|
||||
from bsblan import (
|
||||
BSBLAN,
|
||||
BSBLANAuthError,
|
||||
BSBLANConnectionError,
|
||||
HotWaterState,
|
||||
Sensor,
|
||||
State,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
@@ -62,6 +70,10 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
|
||||
state = await self.client.state()
|
||||
sensor = await self.client.sensor()
|
||||
dhw = await self.client.hot_water_state()
|
||||
except BSBLANAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Authentication failed for BSB-Lan device"
|
||||
) from err
|
||||
except BSBLANConnectionError as err:
|
||||
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
|
||||
raise UpdateFailed(
|
||||
|
||||
@@ -33,14 +33,25 @@
|
||||
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The BSB-Lan integration needs to re-authenticate with {name}",
|
||||
"data": {
|
||||
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.110.0"],
|
||||
"requirements": ["hass-nabucasa==0.111.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from hass_nabucasa import Cloud, cloud_api
|
||||
from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo
|
||||
from hass_nabucasa import (
|
||||
Cloud,
|
||||
MigratePaypalAgreementInfo,
|
||||
PaymentsApiError,
|
||||
SubscriptionInfo,
|
||||
)
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import REQUEST_TIMEOUT
|
||||
@@ -29,17 +31,17 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo
|
||||
|
||||
async def async_migrate_paypal_agreement(
|
||||
cloud: Cloud[CloudClient],
|
||||
) -> dict[str, Any] | None:
|
||||
) -> MigratePaypalAgreementInfo | None:
|
||||
"""Migrate a paypal agreement from legacy."""
|
||||
try:
|
||||
async with asyncio.timeout(REQUEST_TIMEOUT):
|
||||
return await cloud_api.async_migrate_paypal_agreement(cloud)
|
||||
return await cloud.payments.migrate_paypal_agreement()
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"A timeout of %s was reached while trying to start agreement migration",
|
||||
REQUEST_TIMEOUT,
|
||||
)
|
||||
except ClientError as exception:
|
||||
except PaymentsApiError as exception:
|
||||
_LOGGER.error("Failed to start agreement migration - %s", exception)
|
||||
|
||||
return None
|
||||
|
||||
@@ -146,8 +146,9 @@ def _prepare_config_flow_result_json(
|
||||
return prepare_result_json(result)
|
||||
|
||||
data = result.copy()
|
||||
entry: config_entries.ConfigEntry = data["result"]
|
||||
data["result"] = entry.as_json_fragment
|
||||
entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item]
|
||||
# We overwrite the ConfigEntry object with its json representation.
|
||||
data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key]
|
||||
data.pop("data")
|
||||
data.pop("context")
|
||||
return data
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.1.1"],
|
||||
"requirements": ["denonavr==1.1.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
||||
@@ -61,7 +61,7 @@ class DeviceCondition(Condition):
|
||||
self._hass = hass
|
||||
|
||||
@classmethod
|
||||
async def async_validate_condition_config(
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
@@ -69,7 +69,7 @@ class DeviceCondition(Condition):
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
|
||||
async def async_condition_from_config(self) -> condition.ConditionCheckerType:
|
||||
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodiscover==2.7.0",
|
||||
"aiodiscover==2.7.1",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# If path is relative, we assume relative to Home Assistant config dir
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = hass.config.path(download_path)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
|
||||
)
|
||||
|
||||
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
|
||||
@@ -11,6 +11,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
@@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:
|
||||
|
||||
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
||||
download_path = entry.data[CONF_DOWNLOAD_DIR]
|
||||
url: str = service.data[ATTR_URL]
|
||||
subdir: str | None = service.data.get(ATTR_SUBDIR)
|
||||
target_filename: str | None = service.data.get(ATTR_FILENAME)
|
||||
overwrite: bool = service.data[ATTR_OVERWRITE]
|
||||
|
||||
if subdir:
|
||||
# Check the path
|
||||
try:
|
||||
raise_if_invalid_path(subdir)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subdir_invalid",
|
||||
translation_placeholders={"subdir": subdir},
|
||||
) from err
|
||||
if os.path.isabs(subdir):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subdir_not_relative",
|
||||
translation_placeholders={"subdir": subdir},
|
||||
)
|
||||
|
||||
def do_download() -> None:
|
||||
"""Download the file."""
|
||||
final_path = None
|
||||
filename = target_filename
|
||||
try:
|
||||
url = service.data[ATTR_URL]
|
||||
|
||||
subdir = service.data.get(ATTR_SUBDIR)
|
||||
|
||||
filename = service.data.get(ATTR_FILENAME)
|
||||
|
||||
overwrite = service.data.get(ATTR_OVERWRITE)
|
||||
|
||||
if subdir:
|
||||
# Check the path
|
||||
raise_if_invalid_path(subdir)
|
||||
|
||||
final_path = None
|
||||
|
||||
req = requests.get(url, stream=True, timeout=10)
|
||||
|
||||
if req.status_code != HTTPStatus.OK:
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"subdir_invalid": {
|
||||
"message": "Invalid subdirectory, got: {subdir}"
|
||||
},
|
||||
"subdir_not_relative": {
|
||||
"message": "Subdirectory must be relative, got: {subdir}"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"download_file": {
|
||||
"name": "Download file",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/emoncms",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyemoncms==0.1.1"]
|
||||
"requirements": ["pyemoncms==0.1.2"]
|
||||
}
|
||||
|
||||
@@ -12,12 +12,26 @@
|
||||
},
|
||||
"data_description": {
|
||||
"url": "Server URL starting with the protocol (http or https)",
|
||||
"api_key": "Your 32 bits API key"
|
||||
"api_key": "Your 32 bits API key",
|
||||
"sync_mode": "Pick your feeds manually (default) or synchronize them at once"
|
||||
}
|
||||
},
|
||||
"choose_feeds": {
|
||||
"data": {
|
||||
"include_only_feed_id": "Choose feeds to include"
|
||||
},
|
||||
"data_description": {
|
||||
"include_only_feed_id": "Pick the feeds you want to synchronize"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "[%key:component::emoncms::config::step::user::data_description::url%]",
|
||||
"api_key": "[%key:component::emoncms::config::step::user::data_description::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -30,8 +44,8 @@
|
||||
"selector": {
|
||||
"sync_mode": {
|
||||
"options": {
|
||||
"auto": "Synchronize all available Feeds",
|
||||
"manual": "Select which Feeds to synchronize"
|
||||
"auto": "Synchronize all available feeds",
|
||||
"manual": "Select which feeds to synchronize"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -89,6 +103,9 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data::include_only_feed_id%]"
|
||||
},
|
||||
"data_description": {
|
||||
"include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data_description::include_only_feed_id%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ async def async_get_config_entry_diagnostics(
|
||||
entities.append({"entity": entity_dict, "state": state_dict})
|
||||
device_dict = asdict(device)
|
||||
device_dict.pop("_cache", None)
|
||||
# This can be removed when suggested_area is removed from DeviceEntry
|
||||
device_dict.pop("_suggested_area")
|
||||
device_dict.pop("is_new", None)
|
||||
device_entities.append({"device": device_dict, "entities": entities})
|
||||
|
||||
# remove envoy serial
|
||||
|
||||
@@ -316,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
# When port is None (from DHCP discovery), only compare hosts
|
||||
if configured_host == host and (port is None or configured_port == port):
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
# the host matches (and port matches if provided).
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==37.1.5",
|
||||
"aioesphomeapi==37.2.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.1.0"
|
||||
],
|
||||
|
||||
@@ -66,6 +66,26 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
|
||||
key="last_alarm_type_name",
|
||||
translation_key="last_alarm_type_name",
|
||||
),
|
||||
"Record_Mode": SensorEntityDescription(
|
||||
key="Record_Mode",
|
||||
translation_key="record_mode",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"battery_camera_work_mode": SensorEntityDescription(
|
||||
key="battery_camera_work_mode",
|
||||
translation_key="battery_camera_work_mode",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"powerStatus": SensorEntityDescription(
|
||||
key="powerStatus",
|
||||
translation_key="power_status",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"OnlineStatus": SensorEntityDescription(
|
||||
key="OnlineStatus",
|
||||
translation_key="online_status",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -76,16 +96,26 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up EZVIZ sensors based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
entities: list[EzvizSensor] = []
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
for camera, sensors in coordinator.data.items():
|
||||
entities.extend(
|
||||
EzvizSensor(coordinator, camera, sensor)
|
||||
for camera in coordinator.data
|
||||
for sensor, value in coordinator.data[camera].items()
|
||||
if sensor in SENSOR_TYPES
|
||||
if value is not None
|
||||
]
|
||||
)
|
||||
for sensor, value in sensors.items()
|
||||
if sensor in SENSOR_TYPES and value is not None
|
||||
)
|
||||
|
||||
optionals = sensors.get("optionals", {})
|
||||
entities.extend(
|
||||
EzvizSensor(coordinator, camera, optional_key)
|
||||
for optional_key in ("powerStatus", "OnlineStatus")
|
||||
if optional_key in optionals
|
||||
)
|
||||
|
||||
if "mode" in optionals.get("Record_Mode", {}):
|
||||
entities.append(EzvizSensor(coordinator, camera, "mode"))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EzvizSensor(EzvizEntity, SensorEntity):
|
||||
|
||||
@@ -147,6 +147,18 @@
|
||||
},
|
||||
"last_alarm_type_name": {
|
||||
"name": "Last alarm type name"
|
||||
},
|
||||
"record_mode": {
|
||||
"name": "Record mode"
|
||||
},
|
||||
"battery_camera_work_mode": {
|
||||
"name": "Battery work mode"
|
||||
},
|
||||
"power_status": {
|
||||
"name": "Power status"
|
||||
},
|
||||
"online_status": {
|
||||
"name": "Online status"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -106,6 +106,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_logger_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
await self.logger_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -120,6 +121,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_meters_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -129,6 +131,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_ohmpilot_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -138,6 +141,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_power_flow_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,6 +151,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_storages_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -206,6 +211,7 @@ class FroniusSolarNet:
|
||||
logger=_LOGGER,
|
||||
name=_inverter_name,
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250730.0"]
|
||||
"requirements": ["home-assistant-frontend==20250805.0"]
|
||||
}
|
||||
|
||||
@@ -123,10 +123,10 @@
|
||||
},
|
||||
"ai_task_data": {
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service",
|
||||
"reconfigure": "Reconfigure Generate data with AI service"
|
||||
"user": "Add AI task",
|
||||
"reconfigure": "Reconfigure AI task"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"entry_type": "AI task",
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["growattServer"],
|
||||
"requirements": ["growattServer==1.6.0"]
|
||||
"requirements": ["growattServer==1.7.1"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"healthy": "Healthy",
|
||||
"host_os": "Host operating system",
|
||||
"installed_addons": "Installed add-ons",
|
||||
"nameservers": "Nameservers",
|
||||
"supervisor_api": "Supervisor API",
|
||||
"supervisor_version": "Supervisor version",
|
||||
"supported": "Supported",
|
||||
@@ -225,6 +226,10 @@
|
||||
"unsupported_virtualization_image": {
|
||||
"title": "Unsupported system - Incorrect OS image for virtualization",
|
||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_os_version": {
|
||||
"title": "Unsupported system - Home Assistant OS version",
|
||||
"description": "System is unsupported because the Home Assistant OS version in use is not supported. Use the link to learn more and how to fix this."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -54,6 +54,15 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"error": "Unsupported",
|
||||
}
|
||||
|
||||
nameservers = set()
|
||||
for interface in network_info.get("interfaces", []):
|
||||
if not interface.get("primary"):
|
||||
continue
|
||||
if ipv4 := interface.get("ipv4"):
|
||||
nameservers.update(ipv4.get("nameservers", []))
|
||||
if ipv6 := interface.get("ipv6"):
|
||||
nameservers.update(ipv6.get("nameservers", []))
|
||||
|
||||
information = {
|
||||
"host_os": host_info.get("operating_system"),
|
||||
"update_channel": info.get("channel"),
|
||||
@@ -62,6 +71,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"docker_version": info.get("docker"),
|
||||
"disk_total": f"{host_info.get('disk_total')} GB",
|
||||
"disk_used": f"{host_info.get('disk_used')} GB",
|
||||
"nameservers": ", ".join(nameservers),
|
||||
"healthy": healthy,
|
||||
"supported": supported,
|
||||
"host_connectivity": network_info.get("host_internet"),
|
||||
|
||||
@@ -12,6 +12,7 @@ from ha_silabs_firmware_client import (
|
||||
ManifestMissing,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8)
|
||||
class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
"""Coordinator to manage firmware updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
session: ClientSession,
|
||||
url: str,
|
||||
) -> None:
|
||||
"""Initialize the firmware update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
||||
@@ -124,6 +124,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
||||
@@ -129,6 +129,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
||||
@@ -11,10 +11,16 @@ from pyHomee import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
RESULT_CANNOT_CONNECT,
|
||||
RESULT_INVALID_AUTH,
|
||||
RESULT_UNKNOWN_ERROR,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,60 +39,137 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
homee: Homee
|
||||
_host: str
|
||||
_name: str
|
||||
_reauth_host: str
|
||||
_reauth_username: str
|
||||
|
||||
async def _connect_homee(self) -> dict[str, str]:
|
||||
errors: dict[str, str] = {}
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = RESULT_CANNOT_CONNECT
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = RESULT_INVALID_AUTH
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = RESULT_UNKNOWN_ERROR
|
||||
else:
|
||||
_LOGGER.info("Got access token for homee")
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
_LOGGER.debug("Homee task created")
|
||||
await self.homee.wait_until_connected()
|
||||
_LOGGER.info("Homee connected")
|
||||
self.homee.disconnect()
|
||||
_LOGGER.debug("Homee disconnecting")
|
||||
await self.homee.wait_until_disconnected()
|
||||
_LOGGER.info("Homee config successfully tested")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
self.homee.settings.uid, raise_on_progress=self.source != SOURCE_USER
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.info("Created new homee entry with ID %s", self.homee.settings.uid)
|
||||
|
||||
return errors
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial user step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
errors = await self._connect_homee()
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
_LOGGER.info("Got access token for homee")
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
_LOGGER.debug("Homee task created")
|
||||
await self.homee.wait_until_connected()
|
||||
_LOGGER.info("Homee connected")
|
||||
self.homee.disconnect()
|
||||
_LOGGER.debug("Homee disconnecting")
|
||||
await self.homee.wait_until_disconnected()
|
||||
_LOGGER.info("Homee config successfully tested")
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.info(
|
||||
"Created new homee entry with ID %s", self.homee.settings.uid
|
||||
)
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
|
||||
# Ensure that an IPv4 address is received
|
||||
self._host = discovery_info.host
|
||||
self._name = discovery_info.hostname[6:18]
|
||||
if discovery_info.ip_address.version == 6:
|
||||
return self.async_abort(reason="ipv6_address")
|
||||
|
||||
await self.async_set_unique_id(self._name)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
|
||||
|
||||
# Cause an auth-error to see if homee is reachable.
|
||||
self.homee = Homee(
|
||||
self._host,
|
||||
"dummy_username",
|
||||
"dummy_password",
|
||||
)
|
||||
errors = await self._connect_homee()
|
||||
if errors["base"] != RESULT_INVALID_AUTH:
|
||||
return self.async_abort(reason=RESULT_CANNOT_CONNECT)
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._name, "host": self._host}
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the configuration of the device."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self.homee = Homee(
|
||||
self._host,
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
errors = await self._connect_homee()
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
|
||||
data={
|
||||
CONF_HOST: self._host,
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
CONF_HOST: self._name,
|
||||
},
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
@@ -108,12 +191,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
errors["base"] = RESULT_CANNOT_CONNECT
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
errors["base"] = RESULT_INVALID_AUTH
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
errors["base"] = RESULT_UNKNOWN_ERROR
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
@@ -161,12 +244,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
errors["base"] = RESULT_CANNOT_CONNECT
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
errors["base"] = RESULT_INVALID_AUTH
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
errors["base"] = RESULT_UNKNOWN_ERROR
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
|
||||
@@ -20,6 +20,11 @@ from homeassistant.const import (
|
||||
# General
|
||||
DOMAIN = "homee"
|
||||
|
||||
# Error strings
|
||||
RESULT_CANNOT_CONNECT = "cannot_connect"
|
||||
RESULT_INVALID_AUTH = "invalid_auth"
|
||||
RESULT_UNKNOWN_ERROR = "unknown"
|
||||
|
||||
# Sensor mappings
|
||||
HOMEE_UNIT_TO_HA_UNIT = {
|
||||
"": None,
|
||||
|
||||
@@ -8,5 +8,11 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["homee"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyHomee==1.2.10"]
|
||||
"requirements": ["pyHomee==1.2.10"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_ssh._tcp.local.",
|
||||
"name": "homee-*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
"data_description": {
|
||||
"host": "[%key:component::homee::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Configure discovered homee {host}",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::homee::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::homee::config::step::user::data_description::password%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -628,12 +628,12 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
||||
self,
|
||||
domain: str,
|
||||
service: str,
|
||||
service_data: dict[str, Any] | None,
|
||||
service_data: dict[str, Any],
|
||||
value: Any | None = None,
|
||||
) -> None:
|
||||
"""Fire event and call service for changes from HomeKit."""
|
||||
event_data = {
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_ENTITY_ID: service_data.get(ATTR_ENTITY_ID, self.entity_id),
|
||||
ATTR_DISPLAY_NAME: self.display_name,
|
||||
ATTR_SERVICE: service,
|
||||
ATTR_VALUE: value,
|
||||
|
||||
@@ -57,6 +57,8 @@ CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor"
|
||||
CONF_LINKED_OBSTRUCTION_SENSOR = "linked_obstruction_sensor"
|
||||
CONF_LINKED_PM25_SENSOR = "linked_pm25_sensor"
|
||||
CONF_LINKED_TEMPERATURE_SENSOR = "linked_temperature_sensor"
|
||||
CONF_LINKED_VALVE_DURATION = "linked_valve_duration"
|
||||
CONF_LINKED_VALVE_END_TIME = "linked_valve_end_time"
|
||||
CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold"
|
||||
CONF_MAX_FPS = "max_fps"
|
||||
CONF_MAX_HEIGHT = "max_height"
|
||||
@@ -229,10 +231,12 @@ CHAR_ON = "On"
|
||||
CHAR_OUTLET_IN_USE = "OutletInUse"
|
||||
CHAR_POSITION_STATE = "PositionState"
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT = "ProgrammableSwitchEvent"
|
||||
CHAR_REMAINING_DURATION = "RemainingDuration"
|
||||
CHAR_REMOTE_KEY = "RemoteKey"
|
||||
CHAR_ROTATION_DIRECTION = "RotationDirection"
|
||||
CHAR_ROTATION_SPEED = "RotationSpeed"
|
||||
CHAR_SATURATION = "Saturation"
|
||||
CHAR_SET_DURATION = "SetDuration"
|
||||
CHAR_SERIAL_NUMBER = "SerialNumber"
|
||||
CHAR_SERVICE_LABEL_INDEX = "ServiceLabelIndex"
|
||||
CHAR_SERVICE_LABEL_NAMESPACE = "ServiceLabelNamespace"
|
||||
|
||||
@@ -15,6 +15,11 @@ from pyhap.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components import button, input_button
|
||||
from homeassistant.components.input_number import (
|
||||
ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE,
|
||||
DOMAIN as INPUT_NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION
|
||||
from homeassistant.components.lawn_mower import (
|
||||
DOMAIN as LAWN_MOWER_DOMAIN,
|
||||
@@ -45,6 +50,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .accessories import TYPES, HomeAccessory, HomeDriver
|
||||
from .const import (
|
||||
@@ -54,7 +60,11 @@ from .const import (
|
||||
CHAR_NAME,
|
||||
CHAR_ON,
|
||||
CHAR_OUTLET_IN_USE,
|
||||
CHAR_REMAINING_DURATION,
|
||||
CHAR_SET_DURATION,
|
||||
CHAR_VALVE_TYPE,
|
||||
CONF_LINKED_VALVE_DURATION,
|
||||
CONF_LINKED_VALVE_END_TIME,
|
||||
SERV_OUTLET,
|
||||
SERV_SWITCH,
|
||||
SERV_VALVE,
|
||||
@@ -271,7 +281,21 @@ class ValveBase(HomeAccessory):
|
||||
self.on_service = on_service
|
||||
self.off_service = off_service
|
||||
|
||||
serv_valve = self.add_preload_service(SERV_VALVE)
|
||||
self.chars = []
|
||||
|
||||
self.linked_duration_entity: str | None = self.config.get(
|
||||
CONF_LINKED_VALVE_DURATION
|
||||
)
|
||||
self.linked_end_time_entity: str | None = self.config.get(
|
||||
CONF_LINKED_VALVE_END_TIME
|
||||
)
|
||||
|
||||
if self.linked_duration_entity:
|
||||
self.chars.append(CHAR_SET_DURATION)
|
||||
if self.linked_end_time_entity:
|
||||
self.chars.append(CHAR_REMAINING_DURATION)
|
||||
|
||||
serv_valve = self.add_preload_service(SERV_VALVE, self.chars)
|
||||
self.char_active = serv_valve.configure_char(
|
||||
CHAR_ACTIVE, value=False, setter_callback=self.set_state
|
||||
)
|
||||
@@ -279,6 +303,25 @@ class ValveBase(HomeAccessory):
|
||||
self.char_valve_type = serv_valve.configure_char(
|
||||
CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type
|
||||
)
|
||||
|
||||
if CHAR_SET_DURATION in self.chars:
|
||||
_LOGGER.debug(
|
||||
"%s: Add characteristic %s", self.entity_id, CHAR_SET_DURATION
|
||||
)
|
||||
self.char_set_duration = serv_valve.configure_char(
|
||||
CHAR_SET_DURATION,
|
||||
value=self.get_duration(),
|
||||
setter_callback=self.set_duration,
|
||||
)
|
||||
|
||||
if CHAR_REMAINING_DURATION in self.chars:
|
||||
_LOGGER.debug(
|
||||
"%s: Add characteristic %s", self.entity_id, CHAR_REMAINING_DURATION
|
||||
)
|
||||
self.char_remaining_duration = serv_valve.configure_char(
|
||||
CHAR_REMAINING_DURATION, getter_callback=self.get_remaining_duration
|
||||
)
|
||||
|
||||
# Set the state so it is in sync on initial
|
||||
# GET to avoid an event storm after homekit startup
|
||||
self.async_update_state(state)
|
||||
@@ -294,12 +337,75 @@ class ValveBase(HomeAccessory):
|
||||
@callback
|
||||
def async_update_state(self, new_state: State) -> None:
|
||||
"""Update switch state after state changed."""
|
||||
self._update_duration_chars()
|
||||
current_state = 1 if new_state.state in self.open_states else 0
|
||||
_LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state)
|
||||
self.char_active.set_value(current_state)
|
||||
_LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state)
|
||||
self.char_in_use.set_value(current_state)
|
||||
|
||||
def _update_duration_chars(self) -> None:
|
||||
"""Update valve duration related properties if characteristics are available."""
|
||||
if CHAR_SET_DURATION in self.chars:
|
||||
self.char_set_duration.set_value(self.get_duration())
|
||||
if CHAR_REMAINING_DURATION in self.chars:
|
||||
self.char_remaining_duration.set_value(self.get_remaining_duration())
|
||||
|
||||
def set_duration(self, value: int) -> None:
|
||||
"""Set default duration for how long the valve should remain open."""
|
||||
_LOGGER.debug("%s: Set default run time to %s", self.entity_id, value)
|
||||
self.async_call_service(
|
||||
INPUT_NUMBER_DOMAIN,
|
||||
INPUT_NUMBER_SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: self.linked_duration_entity,
|
||||
INPUT_NUMBER_ATTR_VALUE: value,
|
||||
},
|
||||
value,
|
||||
)
|
||||
|
||||
def get_duration(self) -> int:
|
||||
"""Get the default duration from Home Assistant."""
|
||||
duration_state = self._get_entity_state(self.linked_duration_entity)
|
||||
if duration_state is None:
|
||||
_LOGGER.debug(
|
||||
"%s: No linked duration entity state available", self.entity_id
|
||||
)
|
||||
return 0
|
||||
|
||||
try:
|
||||
duration = float(duration_state)
|
||||
return max(int(duration), 0)
|
||||
except ValueError:
|
||||
_LOGGER.debug("%s: Cannot parse linked duration entity", self.entity_id)
|
||||
return 0
|
||||
|
||||
def get_remaining_duration(self) -> int:
|
||||
"""Calculate the remaining duration based on end time in Home Assistant."""
|
||||
end_time_state = self._get_entity_state(self.linked_end_time_entity)
|
||||
if end_time_state is None:
|
||||
_LOGGER.debug(
|
||||
"%s: No linked end time entity state available", self.entity_id
|
||||
)
|
||||
return self.get_duration()
|
||||
|
||||
end_time = dt_util.parse_datetime(end_time_state)
|
||||
if end_time is None:
|
||||
_LOGGER.debug("%s: Cannot parse linked end time entity", self.entity_id)
|
||||
return self.get_duration()
|
||||
|
||||
remaining_time = (end_time - dt_util.utcnow()).total_seconds()
|
||||
return max(int(remaining_time), 0)
|
||||
|
||||
def _get_entity_state(self, entity_id: str | None) -> str | None:
|
||||
"""Fetch the state of a linked entity."""
|
||||
if entity_id is None:
|
||||
return None
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state is None:
|
||||
return None
|
||||
return state.state
|
||||
|
||||
|
||||
@TYPES.register("ValveSwitch")
|
||||
class ValveSwitch(ValveBase):
|
||||
|
||||
@@ -17,6 +17,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import (
|
||||
binary_sensor,
|
||||
input_number,
|
||||
media_player,
|
||||
persistent_notification,
|
||||
sensor,
|
||||
@@ -69,6 +70,8 @@ from .const import (
|
||||
CONF_LINKED_OBSTRUCTION_SENSOR,
|
||||
CONF_LINKED_PM25_SENSOR,
|
||||
CONF_LINKED_TEMPERATURE_SENSOR,
|
||||
CONF_LINKED_VALVE_DURATION,
|
||||
CONF_LINKED_VALVE_END_TIME,
|
||||
CONF_LOW_BATTERY_THRESHOLD,
|
||||
CONF_MAX_FPS,
|
||||
CONF_MAX_HEIGHT,
|
||||
@@ -266,7 +269,9 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||
TYPE_VALVE,
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN),
|
||||
vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -277,6 +282,12 @@ SENSOR_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
VALVE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN),
|
||||
vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN),
|
||||
}
|
||||
)
|
||||
|
||||
HOMEKIT_CHAR_TRANSLATIONS = {
|
||||
0: " ", # nul
|
||||
@@ -360,6 +371,9 @@ def validate_entity_config(values: dict) -> dict[str, dict]:
|
||||
elif domain == "sensor":
|
||||
config = SENSOR_SCHEMA(config)
|
||||
|
||||
elif domain == "valve":
|
||||
config = VALVE_SCHEMA(config)
|
||||
|
||||
else:
|
||||
config = BASIC_INFO_SCHEMA(config)
|
||||
|
||||
|
||||
@@ -283,19 +283,19 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity):
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Return if the cover is closed."""
|
||||
return self._device.doorState == DoorState.CLOSED
|
||||
return self.functional_channel.doorState == DoorState.CLOSED
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self._device.send_door_command_async(DoorCommand.OPEN)
|
||||
await self.functional_channel.async_send_door_command(DoorCommand.OPEN)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
await self._device.send_door_command_async(DoorCommand.CLOSE)
|
||||
await self.functional_channel.async_send_door_command(DoorCommand.CLOSE)
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
await self._device.send_door_command_async(DoorCommand.STOP)
|
||||
await self.functional_channel.async_send_door_command(DoorCommand.STOP)
|
||||
|
||||
|
||||
class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
|
||||
|
||||
@@ -163,6 +163,7 @@ async def async_setup_entry(
|
||||
name="light",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
@@ -197,6 +198,7 @@ async def async_setup_entry(
|
||||
name="group",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
|
||||
@@ -53,6 +53,7 @@ class SensorManager:
|
||||
LOGGER,
|
||||
name="sensor",
|
||||
update_method=self.async_update_data,
|
||||
config_entry=bridge.config_entry,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
|
||||
@@ -21,6 +21,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_reset_cutting_blade_usage_time(
|
||||
session: AutomowerSession,
|
||||
mower_id: str,
|
||||
) -> None:
|
||||
"""Reset cutting blade usage time."""
|
||||
await session.commands.reset_cutting_blade_usage_time(mower_id)
|
||||
|
||||
|
||||
def reset_cutting_blade_usage_time_availability(data: MowerAttributes) -> bool:
|
||||
"""Return True if blade usage time is greater than 0."""
|
||||
value = data.statistics.cutting_blade_usage_time
|
||||
return value is not None and value > 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Automower button entities."""
|
||||
@@ -28,6 +42,7 @@ class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
||||
available_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
||||
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
||||
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
|
||||
poll_after_sending: bool = False
|
||||
|
||||
|
||||
MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
||||
@@ -43,6 +58,14 @@ MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
||||
translation_key="sync_clock",
|
||||
press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
|
||||
),
|
||||
AutomowerButtonEntityDescription(
|
||||
key="reset_cutting_blade_usage_time",
|
||||
translation_key="reset_cutting_blade_usage_time",
|
||||
available_fn=reset_cutting_blade_usage_time_availability,
|
||||
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
|
||||
press_fn=async_reset_cutting_blade_usage_time,
|
||||
poll_after_sending=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -93,3 +116,5 @@ class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity):
|
||||
async def async_press(self) -> None:
|
||||
"""Send a command to the mower."""
|
||||
await self.entity_description.press_fn(self.coordinator.api, self.mower_id)
|
||||
if self.entity_description.poll_after_sending:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
@@ -14,7 +14,7 @@ from aioautomower.exceptions import (
|
||||
HusqvarnaTimeoutError,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from aioautomower.model import MowerDictionary
|
||||
from aioautomower.model import MowerDictionary, MowerStates
|
||||
from aioautomower.session import AutomowerSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -29,7 +29,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
MAX_WS_RECONNECT_TIME = 600
|
||||
SCAN_INTERVAL = timedelta(minutes=8)
|
||||
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
|
||||
|
||||
PONG_TIMEOUT = timedelta(seconds=90)
|
||||
PING_INTERVAL = timedelta(seconds=10)
|
||||
PING_TIMEOUT = timedelta(seconds=5)
|
||||
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
|
||||
|
||||
|
||||
@@ -58,6 +60,9 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
|
||||
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
|
||||
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
|
||||
self.pong: datetime | None = None
|
||||
self.websocket_alive: bool = False
|
||||
self._watchdog_task: asyncio.Task | None = None
|
||||
|
||||
@override
|
||||
@callback
|
||||
@@ -71,6 +76,18 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
await self.api.connect()
|
||||
self.api.register_data_callback(self.handle_websocket_updates)
|
||||
self.ws_connected = True
|
||||
|
||||
def start_watchdog() -> None:
|
||||
if self._watchdog_task is not None and not self._watchdog_task.done():
|
||||
_LOGGER.debug("Cancelling previous watchdog task")
|
||||
self._watchdog_task.cancel()
|
||||
self._watchdog_task = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._pong_watchdog(),
|
||||
"websocket_watchdog",
|
||||
)
|
||||
|
||||
self.api.register_ws_ready_callback(start_watchdog)
|
||||
try:
|
||||
data = await self.api.get_status()
|
||||
except ApiError as err:
|
||||
@@ -93,6 +110,19 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
mower_data.capabilities.work_areas for mower_data in self.data.values()
|
||||
):
|
||||
self._async_add_remove_work_areas()
|
||||
if (
|
||||
not self._should_poll()
|
||||
and self.update_interval is not None
|
||||
and self.websocket_alive
|
||||
):
|
||||
_LOGGER.debug("All mowers inactive and websocket alive: stop polling")
|
||||
self.update_interval = None
|
||||
if self.update_interval is None and self._should_poll():
|
||||
_LOGGER.debug(
|
||||
"Polling re-enabled via WebSocket: at least one mower active"
|
||||
)
|
||||
self.update_interval = SCAN_INTERVAL
|
||||
self.hass.async_create_task(self.async_request_refresh())
|
||||
|
||||
@callback
|
||||
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
|
||||
@@ -161,6 +191,30 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
"reconnect_task",
|
||||
)
|
||||
|
||||
def _should_poll(self) -> bool:
|
||||
"""Return True if at least one mower is connected and at least one is not OFF."""
|
||||
return any(mower.metadata.connected for mower in self.data.values()) and any(
|
||||
mower.mower.state != MowerStates.OFF for mower in self.data.values()
|
||||
)
|
||||
|
||||
async def _pong_watchdog(self) -> None:
|
||||
_LOGGER.debug("Watchdog started")
|
||||
try:
|
||||
while True:
|
||||
_LOGGER.debug("Sending ping")
|
||||
self.websocket_alive = await self.api.send_empty_message()
|
||||
_LOGGER.debug("Ping result: %s", self.websocket_alive)
|
||||
|
||||
await asyncio.sleep(60)
|
||||
_LOGGER.debug("Websocket alive %s", self.websocket_alive)
|
||||
if not self.websocket_alive:
|
||||
_LOGGER.debug("No pong received → restart polling")
|
||||
if self.update_interval is None:
|
||||
self.update_interval = SCAN_INTERVAL
|
||||
await self.async_request_refresh()
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Watchdog cancelled")
|
||||
|
||||
def _async_add_remove_devices(self) -> None:
|
||||
"""Add new devices and remove orphaned devices from the registry."""
|
||||
current_devices = set(self.data)
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"button": {
|
||||
"sync_clock": {
|
||||
"default": "mdi:clock-check-outline"
|
||||
},
|
||||
"reset_cutting_blade_usage_time": {
|
||||
"default": "mdi:saw-blade"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.1.1"]
|
||||
"requirements": ["aioautomower==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ ERROR_KEYS = [
|
||||
"cutting_drive_motor_2_defect",
|
||||
"cutting_drive_motor_3_defect",
|
||||
"cutting_height_blocked",
|
||||
"cutting_height_problem",
|
||||
"cutting_height_problem_curr",
|
||||
"cutting_height_problem_dir",
|
||||
"cutting_height_problem_drive",
|
||||
"cutting_height_problem",
|
||||
"cutting_motor_problem",
|
||||
"cutting_stopped_slope_too_steep",
|
||||
"cutting_system_blocked",
|
||||
@@ -117,7 +117,6 @@ ERROR_KEYS = [
|
||||
"no_accurate_position_from_satellites",
|
||||
"no_confirmed_position",
|
||||
"no_drive",
|
||||
"no_error",
|
||||
"no_loop_signal",
|
||||
"no_power_in_charging_station",
|
||||
"no_response_from_charger",
|
||||
@@ -169,8 +168,8 @@ ERROR_KEYS = [
|
||||
]
|
||||
|
||||
|
||||
ERROR_KEY_LIST = list(
|
||||
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||
ERROR_KEY_LIST = sorted(
|
||||
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
|
||||
)
|
||||
|
||||
INACTIVE_REASONS: list = [
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
},
|
||||
"sync_clock": {
|
||||
"name": "Sync clock"
|
||||
},
|
||||
"reset_cutting_blade_usage_time": {
|
||||
"name": "Reset cutting blade usage time"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.2.1"]
|
||||
"requirements": ["automower-ble==0.2.7"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["imgw_pib==1.5.1"]
|
||||
"requirements": ["imgw_pib==1.5.2"]
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
),
|
||||
}
|
||||
)
|
||||
self.add_suggested_values_to_schema(
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema,
|
||||
{"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}},
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.8.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.7.23.50952"
|
||||
"knx-frontend==2025.8.6.52906"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ class KrakenData:
|
||||
self._hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=self._config_entry,
|
||||
update_method=self.async_update,
|
||||
update_interval=timedelta(
|
||||
seconds=self._config_entry.options[CONF_SCAN_INTERVAL]
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.2"]
|
||||
"requirements": ["pylitterbot==2024.2.3"]
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
"esa_opt_out_state": {
|
||||
"default": "mdi:home-lightning-bolt"
|
||||
},
|
||||
"esa_state": {
|
||||
"default": "mdi:home-lightning-bolt"
|
||||
},
|
||||
"evse_state": {
|
||||
"default": "mdi:ev-station"
|
||||
},
|
||||
|
||||
@@ -285,7 +285,9 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
device_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
lambda x: None
|
||||
if x is None
|
||||
else min(x, 200) / 2 # Matter range (1-200, capped at 200)
|
||||
),
|
||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
|
||||
@@ -140,11 +140,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
self._calculate_features()
|
||||
# optional battery level
|
||||
if VacuumEntityFeature.BATTERY & self._attr_supported_features:
|
||||
self._attr_battery_level = self.get_matter_attribute_value(
|
||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
||||
)
|
||||
# derive state from the run mode + operational state
|
||||
run_mode_raw: int = self.get_matter_attribute_value(
|
||||
clusters.RvcRunMode.Attributes.CurrentMode
|
||||
@@ -188,11 +183,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
supported_features |= VacuumEntityFeature.STATE
|
||||
supported_features |= VacuumEntityFeature.STOP
|
||||
|
||||
# optional battery attribute = battery feature
|
||||
if self.get_matter_attribute_value(
|
||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
||||
):
|
||||
supported_features |= VacuumEntityFeature.BATTERY
|
||||
# optional identify cluster = locate feature (value must be not None or 0)
|
||||
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
|
||||
supported_features |= VacuumEntityFeature.LOCATE
|
||||
@@ -230,7 +220,6 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||
clusters.RvcOperationalState.Attributes.OperationalState,
|
||||
),
|
||||
optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
|
||||
device_type=(device_types.RoboticVacuumCleaner,),
|
||||
allow_none_value=True,
|
||||
),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiomealie==0.10.0"]
|
||||
"requirements": ["aiomealie==0.10.1"]
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
||||
list_id=self._shopping_list_id,
|
||||
note=item.summary.strip() if item.summary else item.summary,
|
||||
position=position,
|
||||
quantity=0.0,
|
||||
)
|
||||
try:
|
||||
await self.coordinator.client.add_shopping_item(new_shopping_item)
|
||||
@@ -174,7 +175,8 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
||||
if list_item.display.strip() != stripped_item_summary:
|
||||
update_shopping_item.note = stripped_item_summary
|
||||
update_shopping_item.position = position
|
||||
update_shopping_item.is_food = False
|
||||
if update_shopping_item.is_food is not None:
|
||||
update_shopping_item.is_food = False
|
||||
update_shopping_item.food_id = None
|
||||
update_shopping_item.quantity = 0.0
|
||||
update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED
|
||||
@@ -249,7 +251,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
||||
mutate_shopping_item.note = item.note
|
||||
mutate_shopping_item.checked = item.checked
|
||||
|
||||
if item.is_food:
|
||||
if item.is_food or item.food_id:
|
||||
mutate_shopping_item.food_id = item.food_id
|
||||
mutate_shopping_item.unit_id = item.unit_id
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2025.06.09"],
|
||||
"requirements": ["yt-dlp[default]==2025.07.21"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France forecast for city {entry.title}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_forecast_forecast,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
@@ -80,6 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France rain for city {entry.title}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_rain,
|
||||
update_interval=SCAN_INTERVAL_RAIN,
|
||||
)
|
||||
@@ -103,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France alert for department {department}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_alert,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
# Setup MieleAPI and coordinator for data fetch
|
||||
coordinator = MieleDataUpdateCoordinator(hass, auth)
|
||||
coordinator = MieleDataUpdateCoordinator(hass, entry, auth)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
@@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = {
|
||||
38: "quick_power_wash",
|
||||
42: "tall_items",
|
||||
44: "power_wash",
|
||||
200: "eco",
|
||||
202: "automatic",
|
||||
203: "comfort_wash",
|
||||
204: "power_wash",
|
||||
205: "intensive",
|
||||
207: "extra_quiet",
|
||||
209: "comfort_wash_plus",
|
||||
210: "gentle",
|
||||
214: "maintenance",
|
||||
215: "rinse_salt",
|
||||
}
|
||||
TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = {
|
||||
-1: "no_program", # Extrapolated from other device types.
|
||||
|
||||
@@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MieleConfigEntry,
|
||||
api: AsyncConfigEntryAuth,
|
||||
) -> None:
|
||||
"""Initialize the Miele data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=120),
|
||||
)
|
||||
|
||||
@@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor):
|
||||
)
|
||||
).name
|
||||
if self.device.state_plate_step
|
||||
else PlatePowerStep.plate_step_0
|
||||
else PlatePowerStep.plate_step_0.name
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if item["parameters"]
|
||||
if item.get("parameters")
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -203,27 +203,27 @@
|
||||
"plate": {
|
||||
"name": "Plate {plate_no}",
|
||||
"state": {
|
||||
"power_step_0": "0",
|
||||
"power_step_warm": "Warming",
|
||||
"power_step_1": "1",
|
||||
"power_step_2": "1\u2022",
|
||||
"power_step_3": "2",
|
||||
"power_step_4": "2\u2022",
|
||||
"power_step_5": "3",
|
||||
"power_step_6": "3\u2022",
|
||||
"power_step_7": "4",
|
||||
"power_step_8": "4\u2022",
|
||||
"power_step_9": "5",
|
||||
"power_step_10": "5\u2022",
|
||||
"power_step_11": "6",
|
||||
"power_step_12": "6\u2022",
|
||||
"power_step_13": "7",
|
||||
"power_step_14": "7\u2022",
|
||||
"power_step_15": "8",
|
||||
"power_step_16": "8\u2022",
|
||||
"power_step_17": "9",
|
||||
"power_step_18": "9\u2022",
|
||||
"power_step_boost": "Boost"
|
||||
"plate_step_0": "0",
|
||||
"plate_step_warm": "Warming",
|
||||
"plate_step_1": "1",
|
||||
"plate_step_2": "1\u2022",
|
||||
"plate_step_3": "2",
|
||||
"plate_step_4": "2\u2022",
|
||||
"plate_step_5": "3",
|
||||
"plate_step_6": "3\u2022",
|
||||
"plate_step_7": "4",
|
||||
"plate_step_8": "4\u2022",
|
||||
"plate_step_9": "5",
|
||||
"plate_step_10": "5\u2022",
|
||||
"plate_step_11": "6",
|
||||
"plate_step_12": "6\u2022",
|
||||
"plate_step_13": "7",
|
||||
"plate_step_14": "7\u2022",
|
||||
"plate_step_15": "8",
|
||||
"plate_step_16": "8\u2022",
|
||||
"plate_step_17": "9",
|
||||
"plate_step_18": "9\u2022",
|
||||
"plate_step_boost": "Boost"
|
||||
}
|
||||
},
|
||||
"drying_step": {
|
||||
@@ -485,6 +485,8 @@
|
||||
"cook_bacon": "Cook bacon",
|
||||
"biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)",
|
||||
"biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)",
|
||||
"comfort_wash": "Comfort wash",
|
||||
"comfort_wash_plus": "Comfort wash plus",
|
||||
"cool_air": "Cool air",
|
||||
"corn_on_the_cob": "Corn on the cob",
|
||||
"cottons": "Cottons",
|
||||
@@ -827,6 +829,7 @@
|
||||
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
|
||||
"rinse": "Rinse",
|
||||
"rinse_out_lint": "Rinse out lint",
|
||||
"rinse_salt": "Rinse salt",
|
||||
"risotto": "Risotto",
|
||||
"ristretto": "Ristretto",
|
||||
"roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)",
|
||||
|
||||
@@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
historic_data_coordinator = MillHistoricDataUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
mill_data_connection=mill_data_connection,
|
||||
)
|
||||
historic_data_coordinator.async_add_listener(lambda: None)
|
||||
|
||||
@@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
*,
|
||||
mill_data_connection: Mill,
|
||||
) -> None:
|
||||
@@ -70,6 +71,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="MillHistoricDataUpdateCoordinator",
|
||||
config_entry=config_entry,
|
||||
)
|
||||
|
||||
async def _async_update_data(self):
|
||||
|
||||
@@ -289,17 +289,23 @@ class MotionTiltDevice(MotionPositionDevice):
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover tilt to a specific position."""
|
||||
angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
async with self._api_lock:
|
||||
@@ -360,11 +366,15 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Open)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Close)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover tilt to a specific position."""
|
||||
angle = kwargs[ATTR_TILT_POSITION]
|
||||
@@ -376,6 +386,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_set_absolute_position(self, **kwargs):
|
||||
"""Move the cover to a specific absolute position (see TDBU)."""
|
||||
angle = kwargs.get(ATTR_TILT_POSITION)
|
||||
@@ -390,6 +402,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
|
||||
class MotionTDBUDevice(MotionBaseDevice):
|
||||
"""Representation of a Motion Top Down Bottom Up blind Device."""
|
||||
|
||||
@@ -42,6 +42,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
|
||||
|
||||
self._requesting_position: CALLBACK_TYPE | None = None
|
||||
self._previous_positions: list[int | dict | None] = []
|
||||
self._previous_angles: list[int | None] = []
|
||||
|
||||
if blind.device_type in DEVICE_TYPES_WIFI:
|
||||
self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI
|
||||
@@ -112,17 +113,27 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
|
||||
"""Request a state update from the blind at a scheduled point in time."""
|
||||
# add the last position to the list and keep the list at max 2 items
|
||||
self._previous_positions.append(self._blind.position)
|
||||
self._previous_angles.append(self._blind.angle)
|
||||
if len(self._previous_positions) > 2:
|
||||
del self._previous_positions[: len(self._previous_positions) - 2]
|
||||
if len(self._previous_angles) > 2:
|
||||
del self._previous_angles[: len(self._previous_angles) - 2]
|
||||
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Update_trigger)
|
||||
|
||||
self.coordinator.async_update_listeners()
|
||||
|
||||
if len(self._previous_positions) < 2 or not all(
|
||||
self._blind.position == prev_position
|
||||
for prev_position in self._previous_positions
|
||||
if (
|
||||
len(self._previous_positions) < 2
|
||||
or not all(
|
||||
self._blind.position == prev_position
|
||||
for prev_position in self._previous_positions
|
||||
)
|
||||
or len(self._previous_angles) < 2
|
||||
or not all(
|
||||
self._blind.angle == prev_angle for prev_angle in self._previous_angles
|
||||
)
|
||||
):
|
||||
# keep updating the position @self._update_interval_moving until the position does not change.
|
||||
self._requesting_position = async_call_later(
|
||||
@@ -132,6 +143,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
|
||||
)
|
||||
else:
|
||||
self._previous_positions = []
|
||||
self._previous_angles = []
|
||||
self._requesting_position = None
|
||||
|
||||
async def async_request_position_till_stop(self, delay: int | None = None) -> None:
|
||||
@@ -140,7 +152,8 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
|
||||
delay = self._update_interval_moving
|
||||
|
||||
self._previous_positions = []
|
||||
if self._blind.position is None:
|
||||
self._previous_angles = []
|
||||
if self._blind.position is None and self._blind.angle is None:
|
||||
return
|
||||
if self._requesting_position is not None:
|
||||
self._requesting_position()
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.29"]
|
||||
"requirements": ["motionblinds==0.6.30"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"mqtt": {
|
||||
"_": {
|
||||
"trigger": "mdi:swap-horizontal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]",
|
||||
"payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]",
|
||||
"payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]",
|
||||
"power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".",
|
||||
"power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)"
|
||||
}
|
||||
@@ -802,17 +802,17 @@
|
||||
"data": {
|
||||
"max_humidity": "Maximum humidity",
|
||||
"min_humidity": "Minimum humidity",
|
||||
"target_humidity_command_template": "Humidity command template",
|
||||
"target_humidity_command_topic": "Humidity command topic",
|
||||
"target_humidity_state_template": "Humidity state template",
|
||||
"target_humidity_state_topic": "Humidity state topic"
|
||||
"target_humidity_command_template": "Target humidity command template",
|
||||
"target_humidity_command_topic": "Target humidity command topic",
|
||||
"target_humidity_state_template": "Target humidity state template",
|
||||
"target_humidity_state_topic": "Target humidity state topic"
|
||||
},
|
||||
"data_description": {
|
||||
"max_humidity": "The maximum target humidity that can be set.",
|
||||
"min_humidity": "The minimum target humidity that can be set.",
|
||||
"target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.",
|
||||
"target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.",
|
||||
"target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)",
|
||||
"target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the humidity state topic with.",
|
||||
"target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.",
|
||||
"target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)"
|
||||
}
|
||||
},
|
||||
@@ -838,7 +838,7 @@
|
||||
"temperature_low_state_topic": "Lower temperature state topic"
|
||||
},
|
||||
"data_description": {
|
||||
"initial": "The climate initalizes with this target temperature.",
|
||||
"initial": "The climate initializes with this target temperature.",
|
||||
"max_temp": "The maximum target temperature that can be set.",
|
||||
"min_temp": "The minimum target temperature that can be set.",
|
||||
"precision": "The precision in degrees the thermostat is working at.",
|
||||
@@ -1104,6 +1104,7 @@
|
||||
},
|
||||
"device_class_sensor": {
|
||||
"options": {
|
||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
@@ -1284,7 +1285,7 @@
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"mqtt": {
|
||||
"_": {
|
||||
"name": "MQTT",
|
||||
"description": "When a specific message is received on a given MQTT topic.",
|
||||
"description_configured": "When an MQTT message has been received",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Describes the format for MQTT triggers
|
||||
|
||||
mqtt:
|
||||
_:
|
||||
fields:
|
||||
payload:
|
||||
example: "on"
|
||||
|
||||
@@ -58,10 +58,10 @@
|
||||
},
|
||||
"ai_task_data": {
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service",
|
||||
"reconfigure": "Reconfigure Generate data with AI service"
|
||||
"user": "Add AI task",
|
||||
"reconfigure": "Reconfigure AI task"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"entry_type": "AI task",
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["openai==1.93.3", "python-open-router==0.3.0"]
|
||||
"requirements": ["openai==1.93.3", "python-open-router==0.3.1"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user