forked from home-assistant/core
Compare commits
128 Commits
2023.6.0b3
...
2023.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78222bd51c | ||
|
|
9f6dab0643 | ||
|
|
4cf9beccd8 | ||
|
|
8f9425f09f | ||
|
|
0fa954040e | ||
|
|
e26b8e11d3 | ||
|
|
ced6968e85 | ||
|
|
44e7243e25 | ||
|
|
bbbc9f646f | ||
|
|
cda784c969 | ||
|
|
34ef89b16b | ||
|
|
f8cfaa6147 | ||
|
|
5da5522481 | ||
|
|
cee8004641 | ||
|
|
e1751647f4 | ||
|
|
f33d671a5d | ||
|
|
254b1fd314 | ||
|
|
89c6494056 | ||
|
|
b52cfd3324 | ||
|
|
6329f6bc07 | ||
|
|
57dd62e7d6 | ||
|
|
203820d836 | ||
|
|
e1c486fc4a | ||
|
|
78bbec0a6e | ||
|
|
ffe35c73b6 | ||
|
|
d2385f97a7 | ||
|
|
bd0b8dc0bc | ||
|
|
3f936993a9 | ||
|
|
e5c5790768 | ||
|
|
905bdd0dd5 | ||
|
|
9cbcfca2cd | ||
|
|
e6b8e4fd48 | ||
|
|
8f437c5833 | ||
|
|
d28d909114 | ||
|
|
f67577ebe0 | ||
|
|
70d33129d4 | ||
|
|
a63ce8100e | ||
|
|
d557c6e43e | ||
|
|
fd0404bb4a | ||
|
|
576cf52573 | ||
|
|
e83f0bb7a5 | ||
|
|
fa8e952324 | ||
|
|
25a4679266 | ||
|
|
f5aa4f5866 | ||
|
|
0083649e43 | ||
|
|
2505de35c9 | ||
|
|
238eebb0b6 | ||
|
|
4cb30e69ac | ||
|
|
ac00977e57 | ||
|
|
b2db849798 | ||
|
|
2c7a176580 | ||
|
|
4dbc408696 | ||
|
|
582fd11a70 | ||
|
|
96cb5ff8b0 | ||
|
|
6029e23ab7 | ||
|
|
3434d74993 | ||
|
|
e091793b6c | ||
|
|
9c8444da0e | ||
|
|
427f0f4bee | ||
|
|
95528f875e | ||
|
|
a5f86bff45 | ||
|
|
d991970754 | ||
|
|
d745b44180 | ||
|
|
602fcd6b1b | ||
|
|
b39b0a960e | ||
|
|
40bb796f03 | ||
|
|
2801ba6cad | ||
|
|
5da0ef36ea | ||
|
|
d861292900 | ||
|
|
a3fda43c64 | ||
|
|
8705a26a1a | ||
|
|
2b1c45c28c | ||
|
|
0cf3825183 | ||
|
|
413e1c97d7 | ||
|
|
3b27a3aabf | ||
|
|
4509e13ceb | ||
|
|
33bf8c600b | ||
|
|
b508875f17 | ||
|
|
ac963a2b6e | ||
|
|
13029cf26f | ||
|
|
f39a6b96ff | ||
|
|
c6a17d6832 | ||
|
|
74c0552a12 | ||
|
|
f24b514c9a | ||
|
|
e1c47fdb61 | ||
|
|
93baf24394 | ||
|
|
a4e236d0b9 | ||
|
|
421fa5b035 | ||
|
|
3d3fecbd23 | ||
|
|
468be632fd | ||
|
|
74ccdcda68 | ||
|
|
5cc61acfb2 | ||
|
|
02d55a8e49 | ||
|
|
f4e3ef6b51 | ||
|
|
7740539df0 | ||
|
|
b077bf9b86 | ||
|
|
23f2898836 | ||
|
|
e6638ca356 | ||
|
|
93d52d8835 | ||
|
|
26e08abb9a | ||
|
|
6a573b507e | ||
|
|
2b39550e55 | ||
|
|
0e50baf007 | ||
|
|
286de1f051 | ||
|
|
3e23996247 | ||
|
|
7a658117bb | ||
|
|
49388eab3a | ||
|
|
e6fcc6b73c | ||
|
|
e00012289d | ||
|
|
f373f1abd5 | ||
|
|
2c43672a8a | ||
|
|
7a6327d7e2 | ||
|
|
ee8f63b9c9 | ||
|
|
28e0f5e104 | ||
|
|
eb036af410 | ||
|
|
4bb6fec1d6 | ||
|
|
dbd5511e5e | ||
|
|
580065e946 | ||
|
|
4a31cb0ad8 | ||
|
|
5a63079c80 | ||
|
|
902bd521d2 | ||
|
|
aff4d537a7 | ||
|
|
4f00cc9faa | ||
|
|
2a99fea1de | ||
|
|
9aeba6221b | ||
|
|
bb2a89f065 | ||
|
|
f92298c6fc | ||
|
|
6ff55a6505 |
@@ -420,7 +420,6 @@ omit =
|
||||
homeassistant/components/gitlab_ci/sensor.py
|
||||
homeassistant/components/gitter/sensor.py
|
||||
homeassistant/components/glances/sensor.py
|
||||
homeassistant/components/goalfeed/*
|
||||
homeassistant/components/goodwe/__init__.py
|
||||
homeassistant/components/goodwe/button.py
|
||||
homeassistant/components/goodwe/coordinator.py
|
||||
|
||||
10
build.yaml
10
build.yaml
@@ -1,11 +1,11 @@
|
||||
image: homeassistant/{arch}-homeassistant
|
||||
shadow_repository: ghcr.io/home-assistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.05.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.05.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -391,6 +391,7 @@ def async_enable_logging(
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
sys.excepthook = lambda *args: logging.getLogger(None).exception(
|
||||
"Uncaught exception", exc_info=args # type: ignore[arg-type]
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -71,7 +72,7 @@ class AbodeSensor(AbodeDevice, SensorEntity):
|
||||
elif description.key == CONST.HUMI_STATUS_KEY:
|
||||
self._attr_native_unit_of_measurement = device.humidity_unit
|
||||
elif description.key == CONST.LUX_STATUS_KEY:
|
||||
self._attr_native_unit_of_measurement = device.lux_unit
|
||||
self._attr_native_unit_of_measurement = LIGHT_LUX
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.1"]
|
||||
"requirements": ["aioairzone==0.6.4"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.1.7"]
|
||||
"requirements": ["aioairzone-cloud==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ async def async_setup_entry(
|
||||
|
||||
tasks = []
|
||||
for heater in data_connection.get_devices():
|
||||
tasks.append(heater.update_device_info())
|
||||
tasks.append(asyncio.create_task(heater.update_device_info()))
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
devs = []
|
||||
|
||||
@@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self.host = discovery_info.host
|
||||
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
|
||||
self.mac = discovery_info.properties.get("bt")
|
||||
assert self.mac
|
||||
if not self.mac:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
await self.async_set_unique_id(format_mac(self.mac))
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.host, CONF_NAME: self.name}
|
||||
|
||||
@@ -430,7 +430,9 @@ INFERRED_UNITS = {
|
||||
" Percent": PERCENTAGE,
|
||||
" Volts": UnitOfElectricPotential.VOLT,
|
||||
" Ampere": UnitOfElectricCurrent.AMPERE,
|
||||
" Amps": UnitOfElectricCurrent.AMPERE,
|
||||
" Volt-Ampere": UnitOfApparentPower.VOLT_AMPERE,
|
||||
" VA": UnitOfApparentPower.VOLT_AMPERE,
|
||||
" Watts": UnitOfPower.WATT,
|
||||
" Hz": UnitOfFrequency.HERTZ,
|
||||
" C": UnitOfTemperature.CELSIUS,
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.17"]
|
||||
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.18"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aiobafi6==0.8.0"],
|
||||
"requirements": ["aiobafi6==0.8.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_api._tcp.local.",
|
||||
|
||||
@@ -173,7 +173,11 @@ async def async_setup_scanner(
|
||||
rssi = await hass.async_add_executor_job(client.request_rssi)
|
||||
client.close()
|
||||
|
||||
tasks.append(see_device(hass, async_see, mac, friendly_name, rssi))
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
see_device(hass, async_see, mac, friendly_name, rssi)
|
||||
)
|
||||
)
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer_connected==0.13.6"]
|
||||
"requirements": ["bimmer-connected==0.13.7"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==2.11.3"]
|
||||
"requirements": ["bthome-ble==2.12.0"]
|
||||
}
|
||||
|
||||
@@ -47,6 +47,15 @@ from .coordinator import (
|
||||
from .device import device_key_to_bluetooth_entity_key
|
||||
|
||||
SENSOR_DESCRIPTIONS = {
|
||||
# Acceleration (m/s²)
|
||||
(
|
||||
BTHomeSensorDeviceClass.ACCELERATION,
|
||||
Units.ACCELERATION_METERS_PER_SQUARE_SECOND,
|
||||
): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.ACCELERATION}_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}",
|
||||
native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Battery (percent)
|
||||
(BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}",
|
||||
@@ -131,6 +140,15 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
# Gyroscope (°/s)
|
||||
(
|
||||
BTHomeSensorDeviceClass.GYROSCOPE,
|
||||
Units.GYROSCOPE_DEGREES_PER_SECOND,
|
||||
): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.GYROSCOPE}_{Units.GYROSCOPE_DEGREES_PER_SECOND}",
|
||||
native_unit_of_measurement=Units.GYROSCOPE_DEGREES_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Humidity in (percent)
|
||||
(BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",
|
||||
@@ -242,6 +260,15 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Timestamp (datetime object)
|
||||
(
|
||||
BTHomeSensorDeviceClass.TIMESTAMP,
|
||||
None,
|
||||
): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.TIMESTAMP}",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# UV index (-)
|
||||
(
|
||||
BTHomeSensorDeviceClass.UV_INDEX,
|
||||
|
||||
@@ -221,6 +221,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
async def on_hass_started(hass: HomeAssistant) -> None:
|
||||
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
|
||||
_LOGGER.info(
|
||||
"Start migration of Alexa settings from v%s to v%s",
|
||||
self._prefs.alexa_settings_version,
|
||||
ALEXA_SETTINGS_VERSION,
|
||||
)
|
||||
if self._prefs.alexa_settings_version < 2 or (
|
||||
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
|
||||
self._prefs.alexa_settings_version < 3
|
||||
@@ -233,6 +238,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
):
|
||||
self._migrate_alexa_entity_settings_v1()
|
||||
|
||||
_LOGGER.info(
|
||||
"Finished migration of Alexa settings from v%s to v%s",
|
||||
self._prefs.alexa_settings_version,
|
||||
ALEXA_SETTINGS_VERSION,
|
||||
)
|
||||
await self._prefs.async_update(
|
||||
alexa_settings_version=ALEXA_SETTINGS_VERSION
|
||||
)
|
||||
|
||||
@@ -108,7 +108,12 @@ def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
if domain in SUPPORTED_DOMAINS:
|
||||
return True
|
||||
|
||||
device_class = get_device_class(hass, entity_id)
|
||||
try:
|
||||
device_class = get_device_class(hass, entity_id)
|
||||
except HomeAssistantError:
|
||||
# The entity no longer exists
|
||||
return False
|
||||
|
||||
if (
|
||||
domain == "binary_sensor"
|
||||
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
|
||||
@@ -208,6 +213,11 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
async def on_hass_started(hass: HomeAssistant) -> None:
|
||||
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
|
||||
_LOGGER.info(
|
||||
"Start migration of Google Assistant settings from v%s to v%s",
|
||||
self._prefs.google_settings_version,
|
||||
GOOGLE_SETTINGS_VERSION,
|
||||
)
|
||||
if self._prefs.google_settings_version < 2 or (
|
||||
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
|
||||
self._prefs.google_settings_version < 3
|
||||
@@ -220,6 +230,11 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
):
|
||||
self._migrate_google_entity_settings_v1()
|
||||
|
||||
_LOGGER.info(
|
||||
"Finished migration of Google Assistant settings from v%s to v%s",
|
||||
self._prefs.google_settings_version,
|
||||
GOOGLE_SETTINGS_VERSION,
|
||||
)
|
||||
await self._prefs.async_update(
|
||||
google_settings_version=GOOGLE_SETTINGS_VERSION
|
||||
)
|
||||
|
||||
@@ -11,16 +11,24 @@ import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
|
||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_COMMAND_CLOSE,
|
||||
@@ -34,15 +42,19 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
SERVICE_RELOAD,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
@@ -74,6 +86,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL
|
||||
): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}
|
||||
)
|
||||
COVER_SCHEMA = vol.Schema(
|
||||
@@ -86,6 +101,9 @@ COVER_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
NOTIFY_SCHEMA = vol.Schema(
|
||||
@@ -106,6 +124,9 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
SWITCH_SCHEMA = vol.Schema(
|
||||
@@ -118,6 +139,9 @@ SWITCH_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
COMBINED_SCHEMA = vol.Schema(
|
||||
@@ -142,22 +166,49 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Command Line from yaml config."""
|
||||
command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, [])
|
||||
|
||||
async def _reload_config(call: Event | ServiceCall) -> None:
|
||||
"""Reload Command Line."""
|
||||
reload_config = await async_integration_yaml_config(hass, "command_line")
|
||||
reset_platforms = async_get_platforms(hass, "command_line")
|
||||
for reset_platform in reset_platforms:
|
||||
_LOGGER.debug("Reload resetting platform: %s", reset_platform.domain)
|
||||
await reset_platform.async_reset()
|
||||
if not reload_config:
|
||||
return
|
||||
await async_load_platforms(hass, reload_config.get(DOMAIN, []), reload_config)
|
||||
|
||||
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
|
||||
|
||||
await async_load_platforms(hass, config.get(DOMAIN, []), config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_load_platforms(
|
||||
hass: HomeAssistant,
|
||||
command_line_config: list[dict[str, dict[str, Any]]],
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Load platforms from yaml."""
|
||||
if not command_line_config:
|
||||
return True
|
||||
return
|
||||
|
||||
_LOGGER.debug("Full config loaded: %s", command_line_config)
|
||||
|
||||
load_coroutines: list[Coroutine[Any, Any, None]] = []
|
||||
platforms: list[Platform] = []
|
||||
reload_configs: list[tuple] = []
|
||||
for platform_config in command_line_config:
|
||||
for platform, _config in platform_config.items():
|
||||
platforms.append(PLATFORM_MAPPING[platform])
|
||||
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms:
|
||||
platforms.append(mapped_platform)
|
||||
_LOGGER.debug(
|
||||
"Loading config %s for platform %s",
|
||||
platform_config,
|
||||
PLATFORM_MAPPING[platform],
|
||||
)
|
||||
reload_configs.append((PLATFORM_MAPPING[platform], _config))
|
||||
load_coroutines.append(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
@@ -168,10 +219,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, platforms)
|
||||
|
||||
if load_coroutines:
|
||||
_LOGGER.debug("Loading platforms: %s", platforms)
|
||||
await asyncio.gather(*load_coroutines)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for custom shell commands to retrieve values."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -18,17 +19,20 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .sensor import CommandSensorData
|
||||
|
||||
DEFAULT_NAME = "Binary Command Sensor"
|
||||
@@ -84,6 +88,9 @@ async def async_setup_platform(
|
||||
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
|
||||
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
|
||||
scan_interval: timedelta = binary_sensor_config.get(
|
||||
CONF_SCAN_INTERVAL, SCAN_INTERVAL
|
||||
)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
@@ -98,15 +105,17 @@ async def async_setup_platform(
|
||||
payload_off,
|
||||
value_template,
|
||||
unique_id,
|
||||
scan_interval,
|
||||
)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class CommandBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a command line binary sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
@@ -116,6 +125,7 @@ class CommandBinarySensor(BinarySensorEntity):
|
||||
payload_off: str,
|
||||
value_template: Template | None,
|
||||
unique_id: str | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self.data = data
|
||||
@@ -126,8 +136,39 @@ class CommandBinarySensor(BinarySensorEntity):
|
||||
self._payload_off = payload_off
|
||||
self._value_template = value_template
|
||||
self._attr_unique_id = unique_id
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._update_entity_state(None)
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Binary Sensor - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
await self.hass.async_add_executor_job(self.data.update)
|
||||
value = self.data.value
|
||||
@@ -141,3 +182,12 @@ class CommandBinarySensor(BinarySensorEntity):
|
||||
self._attr_is_on = True
|
||||
elif value == self._payload_off:
|
||||
self._attr_is_on = False
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity.
|
||||
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self._update_entity_state(dt_util.now())
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_COMMAND_TIMEOUT = "command_timeout"
|
||||
DEFAULT_TIMEOUT = 15
|
||||
DOMAIN = "command_line"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Support for command line covers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -19,21 +20,23 @@ from homeassistant.const import (
|
||||
CONF_COVERS,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .utils import call_shell_with_timeout, check_output_or_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
COVER_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -97,11 +100,12 @@ async def async_setup_platform(
|
||||
value_template,
|
||||
device_config[CONF_COMMAND_TIMEOUT],
|
||||
device_config.get(CONF_UNIQUE_ID),
|
||||
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
|
||||
)
|
||||
)
|
||||
|
||||
if not covers:
|
||||
_LOGGER.error("No covers added")
|
||||
LOGGER.error("No covers added")
|
||||
return
|
||||
|
||||
async_add_entities(covers)
|
||||
@@ -110,6 +114,8 @@ async def async_setup_platform(
|
||||
class CommandCover(CoverEntity):
|
||||
"""Representation a command line cover."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
|
||||
value_template: Template | None,
|
||||
timeout: int,
|
||||
unique_id: str | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the cover."""
|
||||
self._attr_name = name
|
||||
@@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
|
||||
self._value_template = value_template
|
||||
self._timeout = timeout
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_should_poll = bool(command_state)
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self._command_state:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Cover - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
def _move_cover(self, command: str) -> bool:
|
||||
"""Execute the actual commands."""
|
||||
_LOGGER.info("Running command: %s", command)
|
||||
LOGGER.info("Running command: %s", command)
|
||||
|
||||
returncode = call_shell_with_timeout(command, self._timeout)
|
||||
success = returncode == 0
|
||||
|
||||
if not success:
|
||||
_LOGGER.error(
|
||||
LOGGER.error(
|
||||
"Command failed (with return code %s): %s", returncode, command
|
||||
)
|
||||
|
||||
@@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
|
||||
def _query_state(self) -> str | None:
|
||||
"""Query for the state."""
|
||||
if self._command_state:
|
||||
_LOGGER.info("Running state value command: %s", self._command_state)
|
||||
LOGGER.info("Running state value command: %s", self._command_state)
|
||||
return check_output_or_log(self._command_state, self._timeout)
|
||||
if TYPE_CHECKING:
|
||||
return None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Cover %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||
@@ -181,15 +218,26 @@ class CommandCover(CoverEntity):
|
||||
self._state = None
|
||||
if payload:
|
||||
self._state = int(payload)
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
def open_cover(self, **kwargs: Any) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity.
|
||||
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self._update_entity_state(dt_util.now())
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
self._move_cover(self._command_open)
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
|
||||
await self._update_entity_state(None)
|
||||
|
||||
def close_cover(self, **kwargs: Any) -> None:
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
self._move_cover(self._command_close)
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
|
||||
await self._update_entity_state(None)
|
||||
|
||||
def stop_cover(self, **kwargs: Any) -> None:
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
self._move_cover(self._command_stop)
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
|
||||
await self._update_entity_state(None)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -28,15 +29,15 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .utils import check_output_or_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_JSON_ATTRIBUTES = "json_attributes"
|
||||
|
||||
DEFAULT_NAME = "Command Sensor"
|
||||
@@ -88,6 +89,7 @@ async def async_setup_platform(
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
|
||||
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
async_add_entities(
|
||||
@@ -99,15 +101,17 @@ async def async_setup_platform(
|
||||
value_template,
|
||||
json_attributes,
|
||||
unique_id,
|
||||
scan_interval,
|
||||
)
|
||||
],
|
||||
True,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class CommandSensor(SensorEntity):
|
||||
"""Representation of a sensor that is using shell commands."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
@@ -116,6 +120,7 @@ class CommandSensor(SensorEntity):
|
||||
value_template: Template | None,
|
||||
json_attributes: list[str] | None,
|
||||
unique_id: str | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._attr_name = name
|
||||
@@ -126,8 +131,39 @@ class CommandSensor(SensorEntity):
|
||||
self._value_template = value_template
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._attr_unique_id = unique_id
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._update_entity_state(None)
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Sensor - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
await self.hass.async_add_executor_job(self.data.update)
|
||||
value = self.data.value
|
||||
@@ -144,11 +180,11 @@ class CommandSensor(SensorEntity):
|
||||
if k in json_dict
|
||||
}
|
||||
else:
|
||||
_LOGGER.warning("JSON result was not a dictionary")
|
||||
LOGGER.warning("JSON result was not a dictionary")
|
||||
except ValueError:
|
||||
_LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||
LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||
else:
|
||||
_LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
if self._value_template is None:
|
||||
self._attr_native_value = None
|
||||
return
|
||||
@@ -163,6 +199,15 @@ class CommandSensor(SensorEntity):
|
||||
else:
|
||||
self._attr_native_value = value
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity.
|
||||
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self._update_entity_state(dt_util.now())
|
||||
|
||||
|
||||
class CommandSensorData:
|
||||
"""The class for handling the data retrieval."""
|
||||
@@ -191,7 +236,7 @@ class CommandSensorData:
|
||||
args_to_render = {"arguments": args}
|
||||
rendered_args = args_compiled.render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.exception("Error rendering command template: %s", ex)
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
else:
|
||||
rendered_args = None
|
||||
@@ -203,5 +248,5 @@ class CommandSensorData:
|
||||
# Template used. Construct the string used in the shell
|
||||
command = f"{prog} {rendered_args}"
|
||||
|
||||
_LOGGER.debug("Running command: %s", command)
|
||||
LOGGER.debug("Running command: %s", command)
|
||||
self.value = check_output_or_log(command, self.timeout)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Support for custom shell commands to turn a switch on/off."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -20,6 +21,7 @@ from homeassistant.const import (
|
||||
CONF_ICON,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_SWITCHES,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -27,16 +29,17 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .utils import call_shell_with_timeout, check_output_or_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SWITCH_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -112,11 +115,12 @@ async def async_setup_platform(
|
||||
device_config.get(CONF_COMMAND_STATE),
|
||||
value_template,
|
||||
device_config[CONF_COMMAND_TIMEOUT],
|
||||
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
|
||||
)
|
||||
)
|
||||
|
||||
if not switches:
|
||||
_LOGGER.error("No switches added")
|
||||
LOGGER.error("No switches added")
|
||||
return
|
||||
|
||||
async_add_entities(switches)
|
||||
@@ -125,6 +129,8 @@ async def async_setup_platform(
|
||||
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
"""Representation a switch that can be toggled using shell commands."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ConfigType,
|
||||
@@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
command_state: str | None,
|
||||
value_template: Template | None,
|
||||
timeout: int,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(self.hass, config)
|
||||
@@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
self._command_state = command_state
|
||||
self._value_template = value_template
|
||||
self._timeout = timeout
|
||||
self._attr_should_poll = bool(command_state)
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self._command_state:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Cover - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _switch(self, command: str) -> bool:
|
||||
"""Execute the actual commands."""
|
||||
_LOGGER.info("Running command: %s", command)
|
||||
LOGGER.info("Running command: %s", command)
|
||||
|
||||
success = (
|
||||
await self.hass.async_add_executor_job(
|
||||
@@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Command failed: %s", command)
|
||||
LOGGER.error("Command failed: %s", command)
|
||||
|
||||
return success
|
||||
|
||||
def _query_state_value(self, command: str) -> str | None:
|
||||
"""Execute state command for return value."""
|
||||
_LOGGER.info("Running state value command: %s", command)
|
||||
LOGGER.info("Running state value command: %s", command)
|
||||
return check_output_or_log(command, self._timeout)
|
||||
|
||||
def _query_state_code(self, command: str) -> bool:
|
||||
"""Execute state command for return code."""
|
||||
_LOGGER.info("Running state code command: %s", command)
|
||||
LOGGER.info("Running state code command: %s", command)
|
||||
return (
|
||||
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
|
||||
)
|
||||
@@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
if TYPE_CHECKING:
|
||||
return None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Switch %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||
@@ -201,15 +238,25 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
if payload or value:
|
||||
self._attr_is_on = (value or payload).lower() == "true"
|
||||
self._process_manual_data(payload)
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity.
|
||||
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self._update_entity_state(dt_util.now())
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
if await self._switch(self._command_on) and not self._command_state:
|
||||
self._attr_is_on = True
|
||||
self.async_schedule_update_ha_state()
|
||||
await self._update_entity_state(None)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
if await self._switch(self._command_off) and not self._command_state:
|
||||
self._attr_is_on = False
|
||||
self.async_schedule_update_ha_state()
|
||||
await self._update_entity_state(None)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.5.30"]
|
||||
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.5"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pydaikin==2.9.1"],
|
||||
"requirements": ["pydaikin==2.9.0"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ async def async_setup_entry(
|
||||
[
|
||||
DaikinZoneSwitch(daikin_api, zone_id)
|
||||
for zone_id, zone in enumerate(zones)
|
||||
if zone[0] != ("-", "0")
|
||||
if zone != ("-", "0")
|
||||
]
|
||||
)
|
||||
if daikin_api.device.support_advanced_modes:
|
||||
|
||||
@@ -250,7 +250,7 @@ class ElectraClimateEntity(ClimateEntity):
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
raise ValueError("No target temperature provided")
|
||||
|
||||
self._electra_ac_device.set_temperature(temperature)
|
||||
self._electra_ac_device.set_temperature(int(temperature))
|
||||
await self._async_operate_electra_ac()
|
||||
|
||||
def _update_device_attrs(self) -> None:
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["elkm1_lib"],
|
||||
"requirements": ["elkm1-lib==2.2.2"]
|
||||
"requirements": ["elkm1-lib==2.2.5"]
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class Flexit(ClimateEntity):
|
||||
result = float(
|
||||
await self._async_read_int16_from_register(register_type, register)
|
||||
)
|
||||
if result == -1:
|
||||
if not result:
|
||||
return -1
|
||||
return result / 10.0
|
||||
|
||||
@@ -200,6 +200,6 @@ class Flexit(ClimateEntity):
|
||||
result = await self._hub.async_pymodbus_call(
|
||||
self._slave, register, value, CALL_TYPE_WRITE_REGISTER
|
||||
)
|
||||
if result == -1:
|
||||
if not result:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -43,7 +43,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner
|
||||
fgt = FortiOSAPI()
|
||||
|
||||
try:
|
||||
fgt.tokenlogin(host, token, verify_ssl)
|
||||
fgt.tokenlogin(host, token, verify_ssl, None, 12, "root")
|
||||
except ConnectionError as ex:
|
||||
_LOGGER.error("ConnectionError to FortiOS API: %s", ex)
|
||||
return None
|
||||
@@ -77,7 +77,12 @@ class FortiOSDeviceScanner(DeviceScanner):
|
||||
|
||||
def update(self):
|
||||
"""Update clients from the device."""
|
||||
clients_json = self._fgt.monitor("user/device/query", "")
|
||||
clients_json = self._fgt.monitor(
|
||||
"user/device/query",
|
||||
"",
|
||||
parameters={"filter": "format=master_mac|hostname|is_online"},
|
||||
)
|
||||
|
||||
self._clients_json = clients_json
|
||||
|
||||
self._clients = []
|
||||
@@ -85,8 +90,12 @@ class FortiOSDeviceScanner(DeviceScanner):
|
||||
if clients_json:
|
||||
try:
|
||||
for client in clients_json["results"]:
|
||||
if client["is_online"]:
|
||||
self._clients.append(client["mac"].upper())
|
||||
if (
|
||||
"is_online" in client
|
||||
and "master_mac" in client
|
||||
and client["is_online"]
|
||||
):
|
||||
self._clients.append(client["master_mac"].upper())
|
||||
except KeyError as kex:
|
||||
_LOGGER.error("Key not found in clients: %s", kex)
|
||||
|
||||
@@ -106,17 +115,10 @@ class FortiOSDeviceScanner(DeviceScanner):
|
||||
return None
|
||||
|
||||
for client in data["results"]:
|
||||
if client["mac"] == device:
|
||||
try:
|
||||
if "master_mac" in client and client["master_mac"] == device:
|
||||
if "hostname" in client:
|
||||
name = client["hostname"]
|
||||
_LOGGER.debug("Getting device name=%s", name)
|
||||
return name
|
||||
except KeyError as kex:
|
||||
_LOGGER.debug(
|
||||
"No hostname found for %s in client data: %s",
|
||||
device,
|
||||
kex,
|
||||
)
|
||||
return device.replace(":", "_")
|
||||
|
||||
else:
|
||||
name = client["master_mac"].replace(":", "_")
|
||||
return name
|
||||
return None
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20230601.1"]
|
||||
"requirements": ["home-assistant-frontend==20230608.0"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FullyKioskDataUpdateCoordinator
|
||||
@@ -17,6 +18,14 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Fully Kiosk Browser."""
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Fully Kiosk Browser from a config entry."""
|
||||
|
||||
@@ -28,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
coordinator.async_update_listeners()
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"""Services for the Fully Kiosk Browser integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from fullykiosk import FullyKiosk
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
@@ -16,59 +14,53 @@ from .const import (
|
||||
ATTR_APPLICATION,
|
||||
ATTR_URL,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SERVICE_LOAD_URL,
|
||||
SERVICE_START_APPLICATION,
|
||||
)
|
||||
from .coordinator import FullyKioskDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Fully Kiosk Browser integration."""
|
||||
|
||||
async def execute_service(
|
||||
call: ServiceCall,
|
||||
fully_method: Callable,
|
||||
*args: list[str],
|
||||
**kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Execute a Fully service call.
|
||||
|
||||
:param call: {ServiceCall} HA service call.
|
||||
:param fully_method: {Callable} A method of the FullyKiosk class.
|
||||
:param args: Arguments for fully_method.
|
||||
:param kwargs: Key-word arguments for fully_method.
|
||||
:return: None
|
||||
"""
|
||||
LOGGER.debug(
|
||||
"Calling Fully service %s with args: %s, %s", ServiceCall, args, kwargs
|
||||
)
|
||||
async def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[FullyKioskDataUpdateCoordinator]:
|
||||
config_entries = list[ConfigEntry]()
|
||||
registry = dr.async_get(hass)
|
||||
for target in call.data[ATTR_DEVICE_ID]:
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
for key in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(key)
|
||||
if not entry:
|
||||
continue
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
coordinator = hass.data[DOMAIN][key]
|
||||
# fully_method(coordinator.fully, *args, **kwargs) would make
|
||||
# test_services.py fail.
|
||||
await getattr(coordinator.fully, fully_method.__name__)(
|
||||
*args, **kwargs
|
||||
device_entries = list[ConfigEntry]()
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' is not a {DOMAIN} device"
|
||||
)
|
||||
break
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' not found in device registry"
|
||||
)
|
||||
coordinators = list[FullyKioskDataUpdateCoordinator]()
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||
return coordinators
|
||||
|
||||
async def async_load_url(call: ServiceCall) -> None:
|
||||
"""Load a URL on the Fully Kiosk Browser."""
|
||||
await execute_service(call, FullyKiosk.loadUrl, call.data[ATTR_URL])
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.fully.loadUrl(call.data[ATTR_URL])
|
||||
|
||||
async def async_start_app(call: ServiceCall) -> None:
|
||||
"""Start an app on the device."""
|
||||
await execute_service(
|
||||
call, FullyKiosk.startApplication, call.data[ATTR_APPLICATION]
|
||||
)
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.fully.startApplication(call.data[ATTR_APPLICATION])
|
||||
|
||||
# Register all the above services
|
||||
service_mapping = [
|
||||
|
||||
@@ -223,13 +223,6 @@ SENSOR_TYPES = {
|
||||
icon="mdi:docker",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("raid", "used"): GlancesSensorEntityDescription(
|
||||
key="used",
|
||||
type="raid",
|
||||
name_suffix="Raid used",
|
||||
icon="mdi:harddisk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("raid", "available"): GlancesSensorEntityDescription(
|
||||
key="available",
|
||||
type="raid",
|
||||
@@ -237,6 +230,13 @@ SENSOR_TYPES = {
|
||||
icon="mdi:harddisk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("raid", "used"): GlancesSensorEntityDescription(
|
||||
key="used",
|
||||
type="raid",
|
||||
name_suffix="Raid used",
|
||||
icon="mdi:harddisk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -269,36 +269,36 @@ async def async_setup_entry(
|
||||
if sensor_type in ["fs", "sensors", "raid"]:
|
||||
for sensor_label, params in sensors.items():
|
||||
for param in params:
|
||||
sensor_description = SENSOR_TYPES[(sensor_type, param)]
|
||||
if sensor_description := SENSOR_TYPES.get((sensor_type, param)):
|
||||
_migrate_old_unique_ids(
|
||||
hass,
|
||||
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
|
||||
f"{sensor_label}-{sensor_description.key}",
|
||||
)
|
||||
entities.append(
|
||||
GlancesSensor(
|
||||
coordinator,
|
||||
name,
|
||||
sensor_label,
|
||||
sensor_description,
|
||||
)
|
||||
)
|
||||
else:
|
||||
for sensor in sensors:
|
||||
if sensor_description := SENSOR_TYPES.get((sensor_type, sensor)):
|
||||
_migrate_old_unique_ids(
|
||||
hass,
|
||||
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
|
||||
f"{sensor_label}-{sensor_description.key}",
|
||||
f"{coordinator.host}-{name} {sensor_description.name_suffix}",
|
||||
f"-{sensor_description.key}",
|
||||
)
|
||||
entities.append(
|
||||
GlancesSensor(
|
||||
coordinator,
|
||||
name,
|
||||
sensor_label,
|
||||
"",
|
||||
sensor_description,
|
||||
)
|
||||
)
|
||||
else:
|
||||
for sensor in sensors:
|
||||
sensor_description = SENSOR_TYPES[(sensor_type, sensor)]
|
||||
_migrate_old_unique_ids(
|
||||
hass,
|
||||
f"{coordinator.host}-{name} {sensor_description.name_suffix}",
|
||||
f"-{sensor_description.key}",
|
||||
)
|
||||
entities.append(
|
||||
GlancesSensor(
|
||||
coordinator,
|
||||
name,
|
||||
"",
|
||||
sensor_description,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"""Component for the Goalfeed service."""
|
||||
import json
|
||||
|
||||
import pysher
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
# Version downgraded due to regression in library
|
||||
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
|
||||
DOMAIN = "goalfeed"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
GOALFEED_HOST = "feed.goalfeed.ca"
|
||||
GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth"
|
||||
GOALFEED_APP_ID = "bfd4ed98c1ff22c04074"
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Goalfeed component."""
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
def goal_handler(data):
|
||||
"""Handle goal events."""
|
||||
goal = json.loads(json.loads(data))
|
||||
|
||||
hass.bus.fire("goal", event_data=goal)
|
||||
|
||||
def connect_handler(data):
|
||||
"""Handle connection."""
|
||||
post_data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"connection_info": data,
|
||||
}
|
||||
resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json()
|
||||
|
||||
channel = pusher.subscribe("private-goals", resp["auth"])
|
||||
channel.bind("goal", goal_handler)
|
||||
|
||||
pusher = pysher.Pusher(
|
||||
GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST
|
||||
)
|
||||
|
||||
pusher.connection.bind("pusher:connection_established", connect_handler)
|
||||
pusher.connect()
|
||||
|
||||
return True
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "goalfeed",
|
||||
"name": "Goalfeed",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/goalfeed",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysher"],
|
||||
"requirements": ["pysher==1.0.7"]
|
||||
}
|
||||
@@ -243,7 +243,7 @@ class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
|
||||
In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
|
||||
"""
|
||||
if not self.coordinator.last_update_success:
|
||||
self.coordinator.reset_sensor(self._sensor.id)
|
||||
self.coordinator.reset_sensor(self._sensor.id_)
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug("Goodwe reset %s to 0", self.name)
|
||||
next_midnight = dt_util.start_of_local_day(
|
||||
|
||||
@@ -186,7 +186,7 @@ STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id"
|
||||
SOURCE_CLOUD = "cloud"
|
||||
SOURCE_LOCAL = "local"
|
||||
|
||||
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
|
||||
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK, TYPE_THERMOSTAT}
|
||||
|
||||
FAN_SPEEDS = {
|
||||
"5/5": ["High", "Max", "Fast", "5"],
|
||||
|
||||
@@ -7,13 +7,18 @@ import aiohttp
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
@@ -93,6 +98,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
except RefreshError as ex:
|
||||
entry.async_start_reauth(hass)
|
||||
raise ex
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
row_data = {"created": str(datetime.now())} | call.data[DATA]
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
|
||||
@@ -24,7 +24,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
|
||||
|
||||
SENSORS_TYPES = {
|
||||
"name": SensorType("Name", None, "", ["profile", "name"]),
|
||||
"name": SensorType("Name", None, None, ["profile", "name"]),
|
||||
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]),
|
||||
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
|
||||
"mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
|
||||
@@ -35,7 +35,7 @@ SENSORS_TYPES = {
|
||||
"Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]
|
||||
),
|
||||
"gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]),
|
||||
"class": SensorType("Class", "mdi:sword", "", ["stats", "class"]),
|
||||
"class": SensorType("Class", "mdi:sword", None, ["stats", "class"]),
|
||||
}
|
||||
|
||||
TASKS_TYPES = {
|
||||
|
||||
@@ -305,7 +305,11 @@ class SupervisorIssues:
|
||||
|
||||
async def update(self) -> None:
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
data = await self._client.get_resolution_info()
|
||||
try:
|
||||
data = await self._client.get_resolution_info()
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.error("Failed to update supervisor issues: %r", err)
|
||||
return
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
|
||||
@@ -96,19 +96,29 @@ class MultiprotocolAddonManager(AddonManager):
|
||||
) -> None:
|
||||
"""Register a multipan platform."""
|
||||
self._platforms[integration_domain] = platform
|
||||
if self._channel is not None or not await platform.async_using_multipan(hass):
|
||||
|
||||
channel = await platform.async_get_channel(hass)
|
||||
using_multipan = await platform.async_using_multipan(hass)
|
||||
|
||||
_LOGGER.info(
|
||||
"Registering new multipan platform '%s', using multipan: %s, channel: %s",
|
||||
integration_domain,
|
||||
using_multipan,
|
||||
channel,
|
||||
)
|
||||
|
||||
if self._channel is not None or not using_multipan:
|
||||
return
|
||||
|
||||
new_channel = await platform.async_get_channel(hass)
|
||||
if new_channel is None:
|
||||
if channel is None:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Setting multipan channel to %s (source: '%s')",
|
||||
new_channel,
|
||||
channel,
|
||||
integration_domain,
|
||||
)
|
||||
self.async_set_channel(new_channel)
|
||||
self.async_set_channel(channel)
|
||||
|
||||
async def async_change_channel(
|
||||
self, channel: int, delay: float
|
||||
|
||||
@@ -626,10 +626,10 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def pair(
|
||||
self, client_uuid: UUID, client_public: str, client_permissions: int
|
||||
self, client_username_bytes: bytes, client_public: str, client_permissions: int
|
||||
) -> bool:
|
||||
"""Override super function to dismiss setup message if paired."""
|
||||
success = super().pair(client_uuid, client_public, client_permissions)
|
||||
success = super().pair(client_username_bytes, client_public, client_permissions)
|
||||
if success:
|
||||
async_dismiss_setup_message(self.hass, self._entry_id)
|
||||
return cast(bool, success)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.6.0",
|
||||
"HAP-python==4.7.0",
|
||||
"fnv-hash-fast==0.3.1",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==2.6.4"],
|
||||
"requirements": ["aiohomekit==2.6.5"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class ImapMessage:
|
||||
@property
|
||||
def subject(self) -> str:
|
||||
"""Decode the message subject."""
|
||||
decoded_header = decode_header(self.email_message["Subject"])
|
||||
decoded_header = decode_header(self.email_message["Subject"] or "")
|
||||
subject_header = make_header(decoded_header)
|
||||
return str(subject_header)
|
||||
|
||||
|
||||
@@ -302,12 +302,9 @@ class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new option."""
|
||||
if option not in self.options:
|
||||
_LOGGER.warning(
|
||||
"Invalid option: %s (possible options: %s)",
|
||||
option,
|
||||
", ".join(self.options),
|
||||
raise HomeAssistantError(
|
||||
f"Invalid option: {option} (possible options: {', '.join(self.options)})"
|
||||
)
|
||||
return
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
SENSOR_TYPES = {
|
||||
OPEN_CLOSE_SENSOR: BinarySensorDeviceClass.OPENING,
|
||||
@@ -62,7 +62,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.BINARY_SENSOR}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities)
|
||||
async_add_insteon_binary_sensor_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.BINARY_SENSOR,
|
||||
InsteonBinarySensorEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity):
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
FAN_ONLY = "fan_only"
|
||||
|
||||
@@ -71,7 +71,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.CLIMATE}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities)
|
||||
async_add_insteon_climate_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.CLIMATE,
|
||||
InsteonClimateEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonClimateEntity(InsteonEntity, ClimateEntity):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -34,7 +34,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.COVER}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities)
|
||||
async_add_insteon_cover_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.COVER,
|
||||
InsteonCoverEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonCoverEntity(InsteonEntity, CoverEntity):
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.util.percentage import (
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
SPEED_RANGE = (1, 255) # off is not included
|
||||
|
||||
@@ -38,7 +38,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.FAN}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities)
|
||||
async_add_insteon_fan_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.FAN,
|
||||
InsteonFanEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonFanEntity(InsteonEntity, FanEntity):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Utility methods for the Insteon platform."""
|
||||
from collections.abc import Iterable
|
||||
|
||||
from pyinsteon.device_types.device_base import Device
|
||||
from pyinsteon.device_types.ipdb import (
|
||||
AccessControl_Morningstar,
|
||||
ClimateControl_Thermostat,
|
||||
@@ -44,7 +47,7 @@ from pyinsteon.device_types.ipdb import (
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DEVICE_PLATFORM = {
|
||||
DEVICE_PLATFORM: dict[Device, dict[Platform, Iterable[int]]] = {
|
||||
AccessControl_Morningstar: {Platform.LOCK: [1]},
|
||||
DimmableLightingControl: {Platform.LIGHT: [1]},
|
||||
DimmableLightingControl_Dial: {Platform.LIGHT: [1]},
|
||||
@@ -101,11 +104,11 @@ DEVICE_PLATFORM = {
|
||||
}
|
||||
|
||||
|
||||
def get_device_platforms(device):
|
||||
def get_device_platforms(device) -> dict[Platform, Iterable[int]]:
|
||||
"""Return the HA platforms for a device type."""
|
||||
return DEVICE_PLATFORM.get(type(device), {}).keys()
|
||||
return DEVICE_PLATFORM.get(type(device), {})
|
||||
|
||||
|
||||
def get_platform_groups(device, domain) -> dict:
|
||||
"""Return the platforms that a device belongs in."""
|
||||
return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore[attr-defined]
|
||||
def get_device_platform_groups(device: Device, platform: Platform) -> Iterable[int]:
|
||||
"""Return the list of device groups for a platform."""
|
||||
return get_device_platforms(device).get(platform, [])
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
MAX_BRIGHTNESS = 255
|
||||
|
||||
@@ -37,7 +37,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LIGHT}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_light_entities)
|
||||
async_add_insteon_light_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.LIGHT,
|
||||
InsteonDimmerEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonDimmerEntity(InsteonEntity, LightEntity):
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -30,7 +30,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LOCK}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
|
||||
async_add_insteon_lock_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.LOCK,
|
||||
InsteonLockEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonLockEntity(InsteonEntity, LockEntity):
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyinsteon", "pypubsub"],
|
||||
"requirements": [
|
||||
"pyinsteon==1.4.2",
|
||||
"pyinsteon==1.4.3",
|
||||
"insteon-frontend-home-assistant==0.3.5"
|
||||
],
|
||||
"usb": [
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -33,7 +33,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.SWITCH}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities)
|
||||
async_add_insteon_switch_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.SWITCH,
|
||||
InsteonSwitchEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonSwitchEntity(InsteonEntity, SwitchEntity):
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Utilities used by insteon component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyinsteon import devices
|
||||
from pyinsteon.address import Address
|
||||
@@ -30,6 +33,7 @@ from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
ENTITY_MATCH_ALL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -38,6 +42,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_CAT,
|
||||
@@ -78,7 +83,7 @@ from .const import (
|
||||
SRV_X10_ALL_LIGHTS_ON,
|
||||
SRV_X10_ALL_UNITS_OFF,
|
||||
)
|
||||
from .ipdb import get_device_platforms, get_platform_groups
|
||||
from .ipdb import get_device_platform_groups, get_device_platforms
|
||||
from .schemas import (
|
||||
ADD_ALL_LINK_SCHEMA,
|
||||
ADD_DEFAULT_LINKS_SCHEMA,
|
||||
@@ -89,6 +94,9 @@ from .schemas import (
|
||||
X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .insteon_entity import InsteonEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -107,8 +115,8 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
|
||||
"""Register Insteon device events."""
|
||||
|
||||
@callback
|
||||
def async_fire_group_on_off_event(
|
||||
name: str, address: Address, group: int, button: str
|
||||
def async_fire_insteon_event(
|
||||
name: str, address: Address, group: int, button: str | None = None
|
||||
):
|
||||
# Firing an event when a button is pressed.
|
||||
if button and button[-2] == "_":
|
||||
@@ -132,12 +140,15 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
|
||||
_LOGGER.debug("Firing event %s with %s", event, schema)
|
||||
hass.bus.async_fire(event, schema)
|
||||
|
||||
if str(device.address).startswith("X10"):
|
||||
return
|
||||
|
||||
for name_or_group, event in device.events.items():
|
||||
if isinstance(name_or_group, int):
|
||||
for _, event in device.events[name_or_group].items():
|
||||
_register_event(event, async_fire_group_on_off_event)
|
||||
_register_event(event, async_fire_insteon_event)
|
||||
else:
|
||||
_register_event(event, async_fire_group_on_off_event)
|
||||
_register_event(event, async_fire_insteon_event)
|
||||
|
||||
|
||||
def register_new_device_callback(hass):
|
||||
@@ -158,8 +169,10 @@ def register_new_device_callback(hass):
|
||||
await device.async_status()
|
||||
platforms = get_device_platforms(device)
|
||||
for platform in platforms:
|
||||
groups = get_device_platform_groups(device, platform)
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
|
||||
dispatcher_send(hass, signal, {"address": device.address})
|
||||
dispatcher_send(hass, signal, {"address": device.address, "groups": groups})
|
||||
add_insteon_events(hass, device)
|
||||
|
||||
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
|
||||
|
||||
@@ -383,20 +396,38 @@ def print_aldb_to_log(aldb):
|
||||
|
||||
@callback
|
||||
def async_add_insteon_entities(
|
||||
hass, platform, entity_type, async_add_entities, discovery_info
|
||||
):
|
||||
"""Add Insteon devices to a platform."""
|
||||
new_entities = []
|
||||
device_list = [discovery_info.get("address")] if discovery_info else devices
|
||||
|
||||
for address in device_list:
|
||||
device = devices[address]
|
||||
groups = get_platform_groups(device, platform)
|
||||
for group in groups:
|
||||
new_entities.append(entity_type(device, group))
|
||||
hass: HomeAssistant,
|
||||
platform: Platform,
|
||||
entity_type: type[InsteonEntity],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: dict[str, Any],
|
||||
) -> None:
|
||||
"""Add an Insteon group to a platform."""
|
||||
address = discovery_info["address"]
|
||||
device = devices[address]
|
||||
new_entities = [
|
||||
entity_type(device=device, group=group) for group in discovery_info["groups"]
|
||||
]
|
||||
async_add_entities(new_entities)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_insteon_devices(
|
||||
hass: HomeAssistant,
|
||||
platform: Platform,
|
||||
entity_type: type[InsteonEntity],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add all entities to a platform."""
|
||||
for address in devices:
|
||||
device = devices[address]
|
||||
groups = get_device_platform_groups(device, platform)
|
||||
discovery_info = {"address": address, "groups": groups}
|
||||
async_add_insteon_entities(
|
||||
hass, platform, entity_type, async_add_entities, discovery_info
|
||||
)
|
||||
|
||||
|
||||
def get_usb_ports() -> dict[str, str]:
|
||||
"""Return a dict of USB ports and their friendly names."""
|
||||
ports = list_ports.comports()
|
||||
|
||||
@@ -19,21 +19,18 @@ PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up IPP from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
|
||||
# Create IPP instance for this entry
|
||||
coordinator = IPPDataUpdateCoordinator(
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
base_path=entry.data[CONF_BASE_PATH],
|
||||
tls=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
coordinator = IPPDataUpdateCoordinator(
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
base_path=entry.data[CONF_BASE_PATH],
|
||||
tls=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
@@ -41,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Receive signals from a keyboard and use it as a remote control."""
|
||||
# pylint: disable=import-error
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import aionotify
|
||||
from asyncinotify import Inotify, Mask
|
||||
from evdev import InputDevice, categorize, ecodes, list_devices
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the keyboard_remote."""
|
||||
config = config[DOMAIN]
|
||||
domain_config: list[dict[str, Any]] = config[DOMAIN]
|
||||
|
||||
remote = KeyboardRemote(hass, config)
|
||||
remote = KeyboardRemote(hass, domain_config)
|
||||
remote.setup()
|
||||
|
||||
return True
|
||||
@@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class KeyboardRemote:
|
||||
"""Manage device connection/disconnection using inotify to asynchronously monitor."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, hass: HomeAssistant, config: list[dict[str, Any]]) -> None:
|
||||
"""Create handlers and setup dictionaries to keep track of them."""
|
||||
self.hass = hass
|
||||
self.handlers_by_name = {}
|
||||
self.handlers_by_descriptor = {}
|
||||
self.active_handlers_by_descriptor = {}
|
||||
self.active_handlers_by_descriptor: dict[str, asyncio.Future] = {}
|
||||
self.inotify = None
|
||||
self.watcher = None
|
||||
self.monitor_task = None
|
||||
|
||||
@@ -110,16 +114,12 @@ class KeyboardRemote:
|
||||
connected, and start monitoring for device connection/disconnection.
|
||||
"""
|
||||
|
||||
# start watching
|
||||
self.watcher = aionotify.Watcher()
|
||||
self.watcher.watch(
|
||||
alias="devinput",
|
||||
path=DEVINPUT,
|
||||
flags=aionotify.Flags.CREATE
|
||||
| aionotify.Flags.ATTRIB
|
||||
| aionotify.Flags.DELETE,
|
||||
_LOGGER.debug("Start monitoring")
|
||||
|
||||
self.inotify = Inotify()
|
||||
self.watcher = self.inotify.add_watch(
|
||||
DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE
|
||||
)
|
||||
await self.watcher.setup(self.hass.loop)
|
||||
|
||||
# add initial devices (do this AFTER starting watcher in order to
|
||||
# avoid race conditions leading to missing device connections)
|
||||
@@ -134,7 +134,9 @@ class KeyboardRemote:
|
||||
continue
|
||||
|
||||
self.active_handlers_by_descriptor[descriptor] = handler
|
||||
initial_start_monitoring.add(handler.async_start_monitoring(dev))
|
||||
initial_start_monitoring.add(
|
||||
asyncio.create_task(handler.async_device_start_monitoring(dev))
|
||||
)
|
||||
|
||||
if initial_start_monitoring:
|
||||
await asyncio.wait(initial_start_monitoring)
|
||||
@@ -146,6 +148,10 @@ class KeyboardRemote:
|
||||
|
||||
_LOGGER.debug("Cleanup on shutdown")
|
||||
|
||||
if self.inotify and self.watcher:
|
||||
self.inotify.rm_watch(self.watcher)
|
||||
self.watcher = None
|
||||
|
||||
if self.monitor_task is not None:
|
||||
if not self.monitor_task.done():
|
||||
self.monitor_task.cancel()
|
||||
@@ -153,11 +159,16 @@ class KeyboardRemote:
|
||||
|
||||
handler_stop_monitoring = set()
|
||||
for handler in self.active_handlers_by_descriptor.values():
|
||||
handler_stop_monitoring.add(handler.async_stop_monitoring())
|
||||
|
||||
handler_stop_monitoring.add(
|
||||
asyncio.create_task(handler.async_device_stop_monitoring())
|
||||
)
|
||||
if handler_stop_monitoring:
|
||||
await asyncio.wait(handler_stop_monitoring)
|
||||
|
||||
if self.inotify:
|
||||
self.inotify.close()
|
||||
self.inotify = None
|
||||
|
||||
def get_device_handler(self, descriptor):
|
||||
"""Find the correct device handler given a descriptor (path)."""
|
||||
|
||||
@@ -187,20 +198,21 @@ class KeyboardRemote:
|
||||
async def async_monitor_devices(self):
|
||||
"""Monitor asynchronously for device connection/disconnection or permissions changes."""
|
||||
|
||||
_LOGGER.debug("Start monitoring loop")
|
||||
|
||||
try:
|
||||
while True:
|
||||
event = await self.watcher.get_event()
|
||||
async for event in self.inotify:
|
||||
descriptor = f"{DEVINPUT}/{event.name}"
|
||||
_LOGGER.debug("got events for %s: %s", descriptor, event.mask)
|
||||
|
||||
descriptor_active = descriptor in self.active_handlers_by_descriptor
|
||||
|
||||
if (event.flags & aionotify.Flags.DELETE) and descriptor_active:
|
||||
if (event.mask & Mask.DELETE) and descriptor_active:
|
||||
handler = self.active_handlers_by_descriptor[descriptor]
|
||||
del self.active_handlers_by_descriptor[descriptor]
|
||||
await handler.async_stop_monitoring()
|
||||
await handler.async_device_stop_monitoring()
|
||||
elif (
|
||||
(event.flags & aionotify.Flags.CREATE)
|
||||
or (event.flags & aionotify.Flags.ATTRIB)
|
||||
(event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB)
|
||||
) and not descriptor_active:
|
||||
dev, handler = await self.hass.async_add_executor_job(
|
||||
self.get_device_handler, descriptor
|
||||
@@ -208,31 +220,32 @@ class KeyboardRemote:
|
||||
if handler is None:
|
||||
continue
|
||||
self.active_handlers_by_descriptor[descriptor] = handler
|
||||
await handler.async_start_monitoring(dev)
|
||||
await handler.async_device_start_monitoring(dev)
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Monitoring canceled")
|
||||
return
|
||||
|
||||
class DeviceHandler:
|
||||
"""Manage input events using evdev with asyncio."""
|
||||
|
||||
def __init__(self, hass, dev_block):
|
||||
def __init__(self, hass: HomeAssistant, dev_block: dict[str, Any]) -> None:
|
||||
"""Fill configuration data."""
|
||||
|
||||
self.hass = hass
|
||||
|
||||
key_types = dev_block.get(TYPE)
|
||||
key_types = dev_block[TYPE]
|
||||
|
||||
self.key_values = set()
|
||||
for key_type in key_types:
|
||||
self.key_values.add(KEY_VALUE[key_type])
|
||||
|
||||
self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD)
|
||||
self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY)
|
||||
self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT)
|
||||
self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD]
|
||||
self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY]
|
||||
self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT]
|
||||
self.monitor_task = None
|
||||
self.dev = None
|
||||
|
||||
async def async_keyrepeat(self, path, name, code, delay, repeat):
|
||||
async def async_device_keyrepeat(self, path, name, code, delay, repeat):
|
||||
"""Emulate keyboard delay/repeat behaviour by sending key events on a timer."""
|
||||
|
||||
await asyncio.sleep(delay)
|
||||
@@ -248,8 +261,9 @@ class KeyboardRemote:
|
||||
)
|
||||
await asyncio.sleep(repeat)
|
||||
|
||||
async def async_start_monitoring(self, dev):
|
||||
async def async_device_start_monitoring(self, dev):
|
||||
"""Start event monitoring task and issue event."""
|
||||
_LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name)
|
||||
if self.monitor_task is None:
|
||||
self.dev = dev
|
||||
self.monitor_task = self.hass.async_create_task(
|
||||
@@ -261,7 +275,7 @@ class KeyboardRemote:
|
||||
)
|
||||
_LOGGER.debug("Keyboard (re-)connected, %s", dev.name)
|
||||
|
||||
async def async_stop_monitoring(self):
|
||||
async def async_device_stop_monitoring(self):
|
||||
"""Stop event monitoring task and issue event."""
|
||||
if self.monitor_task is not None:
|
||||
with suppress(OSError):
|
||||
@@ -295,6 +309,7 @@ class KeyboardRemote:
|
||||
_LOGGER.debug("Start device monitoring")
|
||||
await self.hass.async_add_executor_job(dev.grab)
|
||||
async for event in dev.async_read_loop():
|
||||
# pylint: disable=no-member
|
||||
if event.type is ecodes.EV_KEY:
|
||||
if event.value in self.key_values:
|
||||
_LOGGER.debug(categorize(event))
|
||||
@@ -313,7 +328,7 @@ class KeyboardRemote:
|
||||
and self.emulate_key_hold
|
||||
):
|
||||
repeat_tasks[event.code] = self.hass.async_create_task(
|
||||
self.async_keyrepeat(
|
||||
self.async_device_keyrepeat(
|
||||
dev.path,
|
||||
dev.name,
|
||||
event.code,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/keyboard_remote",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aionotify", "evdev"],
|
||||
"requirements": ["evdev==1.4.0", "aionotify==0.2.0"]
|
||||
"requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"]
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ async def async_attach_trigger(
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
|
||||
job = HassJob(action, f"KNX device trigger {trigger_info}")
|
||||
knx: KNXModule = hass.data[DOMAIN]
|
||||
@@ -95,7 +96,7 @@ async def async_attach_trigger(
|
||||
return
|
||||
hass.async_run_hass_job(
|
||||
job,
|
||||
{"trigger": telegram},
|
||||
{"trigger": {**trigger_data, **telegram}},
|
||||
)
|
||||
|
||||
return knx.telegrams.async_listen_telegram(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.10.0",
|
||||
"xknxproject==3.1.0",
|
||||
"knx_frontend==2023.5.31.141540"
|
||||
"xknxproject==3.1.1",
|
||||
"knx-frontend==2023.6.9.195839"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from knx_frontend import get_build_id, locate_dir
|
||||
from knx_frontend import entrypoint_js, is_dev_build, locate_dir
|
||||
import voluptuous as vol
|
||||
from xknx.telegram import TelegramDirection
|
||||
from xknxproject.exceptions import XknxProjectException
|
||||
@@ -31,9 +31,10 @@ async def register_panel(hass: HomeAssistant) -> None:
|
||||
|
||||
if DOMAIN not in hass.data.get("frontend_panels", {}):
|
||||
path = locate_dir()
|
||||
build_id = get_build_id()
|
||||
hass.http.register_static_path(
|
||||
URL_BASE, path, cache_headers=(build_id != "dev")
|
||||
URL_BASE,
|
||||
path,
|
||||
cache_headers=not is_dev_build,
|
||||
)
|
||||
await panel_custom.async_register_panel(
|
||||
hass=hass,
|
||||
@@ -41,12 +42,13 @@ async def register_panel(hass: HomeAssistant) -> None:
|
||||
webcomponent_name="knx-frontend",
|
||||
sidebar_title=DOMAIN.upper(),
|
||||
sidebar_icon="mdi:bus-electric",
|
||||
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
||||
module_url=f"{URL_BASE}/{entrypoint_js()}",
|
||||
embed_iframe=True,
|
||||
require_admin=True,
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "knx/info",
|
||||
@@ -129,6 +131,7 @@ async def ws_project_file_remove(
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "knx/group_monitor_info",
|
||||
@@ -155,6 +158,7 @@ def ws_group_monitor_info(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "knx/subscribe_telegrams",
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pylast import LastFMNetwork, User, WSError
|
||||
from pylast import LastFMNetwork, PyLastError, User, WSError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -128,11 +128,14 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
main_user, _ = get_lastfm_user(
|
||||
self.data[CONF_API_KEY], self.data[CONF_MAIN_USER]
|
||||
)
|
||||
friends_response = await self.hass.async_add_executor_job(
|
||||
main_user.get_friends
|
||||
)
|
||||
friends = [
|
||||
SelectOptionDict(value=friend.name, label=friend.get_name(True))
|
||||
for friend in main_user.get_friends()
|
||||
for friend in friends_response
|
||||
]
|
||||
except WSError:
|
||||
except PyLastError:
|
||||
friends = []
|
||||
return self.async_show_form(
|
||||
step_id="friends",
|
||||
@@ -197,11 +200,14 @@ class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
self.options[CONF_API_KEY],
|
||||
self.options[CONF_MAIN_USER],
|
||||
)
|
||||
friends_response = await self.hass.async_add_executor_job(
|
||||
main_user.get_friends
|
||||
)
|
||||
friends = [
|
||||
SelectOptionDict(value=friend.name, label=friend.get_name(True))
|
||||
for friend in main_user.get_friends()
|
||||
for friend in friends_response
|
||||
]
|
||||
except WSError:
|
||||
except PyLastError:
|
||||
friends = []
|
||||
else:
|
||||
friends = []
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
|
||||
from pylast import LastFMNetwork, Track, User, WSError
|
||||
from pylast import LastFMNetwork, PyLastError, Track, User
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
@@ -104,26 +104,30 @@ class LastFmSensor(SensorEntity):
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update device state."""
|
||||
self._attr_native_value = STATE_NOT_SCROBBLING
|
||||
try:
|
||||
self._user.get_playcount()
|
||||
except WSError as exc:
|
||||
play_count = self._user.get_playcount()
|
||||
self._attr_entity_picture = self._user.get_image()
|
||||
now_playing = self._user.get_now_playing()
|
||||
top_tracks = self._user.get_top_tracks(limit=1)
|
||||
last_tracks = self._user.get_recent_tracks(limit=1)
|
||||
except PyLastError as exc:
|
||||
self._attr_available = False
|
||||
LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc)
|
||||
return
|
||||
self._attr_entity_picture = self._user.get_image()
|
||||
if now_playing := self._user.get_now_playing():
|
||||
self._attr_available = True
|
||||
if now_playing:
|
||||
self._attr_native_value = format_track(now_playing)
|
||||
else:
|
||||
self._attr_native_value = STATE_NOT_SCROBBLING
|
||||
top_played = None
|
||||
if top_tracks := self._user.get_top_tracks(limit=1):
|
||||
top_played = format_track(top_tracks[0].item)
|
||||
last_played = None
|
||||
if last_tracks := self._user.get_recent_tracks(limit=1):
|
||||
last_played = format_track(last_tracks[0].track)
|
||||
play_count = self._user.get_playcount()
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_LAST_PLAYED: last_played,
|
||||
ATTR_PLAY_COUNT: play_count,
|
||||
ATTR_TOP_PLAYED: top_played,
|
||||
ATTR_LAST_PLAYED: None,
|
||||
ATTR_TOP_PLAYED: None,
|
||||
}
|
||||
if len(last_tracks) > 0:
|
||||
self._attr_extra_state_attributes[ATTR_LAST_PLAYED] = format_track(
|
||||
last_tracks[0].track
|
||||
)
|
||||
if len(top_tracks) > 0:
|
||||
self._attr_extra_state_attributes[ATTR_TOP_PLAYED] = format_track(
|
||||
top_tracks[0].item
|
||||
)
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"requirements": ["pylitterbot==2023.4.0"]
|
||||
"requirements": ["pylitterbot==2023.4.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==4.5.1"]
|
||||
"requirements": ["ical==4.5.4"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from . import websocket_api
|
||||
from .const import (
|
||||
ATTR_LEVEL,
|
||||
DEFAULT_LOGSEVERITY,
|
||||
DOMAIN,
|
||||
LOGGER_DEFAULT,
|
||||
LOGGER_FILTERS,
|
||||
@@ -39,9 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY
|
||||
): _VALID_LOG_LEVEL,
|
||||
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
|
||||
vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}),
|
||||
vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}),
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class LoggerSettings:
|
||||
|
||||
self._yaml_config = yaml_config
|
||||
self._default_level = logging.INFO
|
||||
if DOMAIN in yaml_config:
|
||||
if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
|
||||
self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
|
||||
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
|
||||
hass, STORAGE_VERSION, STORAGE_KEY
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
|
||||
import async_timeout
|
||||
from matter_server.client import MatterClient
|
||||
from matter_server.client.exceptions import CannotConnect, InvalidServerVersion
|
||||
from matter_server.common.errors import MatterError, NodeCommissionFailed
|
||||
from matter_server.common.errors import MatterError, NodeCommissionFailed, NodeNotExists
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
@@ -207,7 +208,9 @@ async def async_remove_config_entry_device(
|
||||
)
|
||||
|
||||
matter = get_matter(hass)
|
||||
await matter.matter_client.remove_node(node.node_id)
|
||||
with suppress(NodeNotExists):
|
||||
# ignore if the server has already removed the node.
|
||||
await matter.matter_client.remove_node(node.node_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class MatterAdapter:
|
||||
get_clean_name(basic_info.nodeLabel)
|
||||
or get_clean_name(basic_info.productLabel)
|
||||
or get_clean_name(basic_info.productName)
|
||||
or device_type.__class__.__name__
|
||||
or device_type.__name__
|
||||
if device_type
|
||||
else None
|
||||
)
|
||||
@@ -117,7 +117,7 @@ class MatterAdapter:
|
||||
identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}"))
|
||||
|
||||
model = (
|
||||
get_clean_name(basic_info.productName) or device_type.__class__.__name__
|
||||
get_clean_name(basic_info.productName) or device_type.__name__
|
||||
if device_type
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==3.4.1"]
|
||||
"requirements": ["python-matter-server==3.5.1"]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
class LocalSource(MediaSource):
|
||||
"""Provide local directories as media sources."""
|
||||
|
||||
name: str = "Local Media"
|
||||
name: str = "My media"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize local source."""
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/melnor",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["melnor-bluetooth==0.0.22"]
|
||||
"requirements": ["melnor-bluetooth==0.0.25"]
|
||||
}
|
||||
|
||||
@@ -133,10 +133,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator_alert.async_refresh()
|
||||
|
||||
if not coordinator_alert.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][department] = True
|
||||
if coordinator_alert.last_update_success:
|
||||
hass.data[DOMAIN][department] = True
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
@@ -158,11 +156,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
COORDINATOR_FORECAST: coordinator_forecast,
|
||||
COORDINATOR_RAIN: coordinator_rain,
|
||||
COORDINATOR_ALERT: coordinator_alert,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
if coordinator_alert and coordinator_alert.last_update_success:
|
||||
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/noaa_tides",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["noaa_coops"],
|
||||
"requirements": ["noaa-coops==0.1.8"]
|
||||
"requirements": ["noaa-coops==0.1.9"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nuki",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pynuki"],
|
||||
"requirements": ["pynuki==1.6.1"]
|
||||
"requirements": ["pynuki==1.6.2"]
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": ["@joostlek"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/opensky",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-opensky==0.0.7"]
|
||||
"requirements": ["python-opensky==0.0.9"]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ def setup_platform(
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
radius = config.get(CONF_RADIUS, 0)
|
||||
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius)
|
||||
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius * 1000)
|
||||
session = async_get_clientsession(hass)
|
||||
opensky = OpenSky(session=session)
|
||||
add_entities(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opple",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyoppleio"],
|
||||
"requirements": ["pyoppleio==1.0.5"]
|
||||
"requirements": ["pyoppleio-legacy==1.0.8"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.1.0"]
|
||||
"requirements": ["python-otbr-api==2.2.0"]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ class OTBRData:
|
||||
"""Create an active operational dataset."""
|
||||
return await self.api.create_active_dataset(dataset)
|
||||
|
||||
@_handle_otbr_error
|
||||
async def delete_active_dataset(self) -> None:
|
||||
"""Delete the active operational dataset."""
|
||||
return await self.api.delete_active_dataset()
|
||||
|
||||
@_handle_otbr_error
|
||||
async def set_active_dataset_tlvs(self, dataset: bytes) -> None:
|
||||
"""Set current active operational dataset in TLVS format."""
|
||||
|
||||
@@ -81,6 +81,12 @@ async def websocket_create_network(
|
||||
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
|
||||
return
|
||||
|
||||
try:
|
||||
await data.delete_active_dataset()
|
||||
except HomeAssistantError as exc:
|
||||
connection.send_error(msg["id"], "delete_active_dataset_failed", str(exc))
|
||||
return
|
||||
|
||||
try:
|
||||
await data.create_active_dataset(
|
||||
python_otbr_api.ActiveDataSet(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.7.9"],
|
||||
"requirements": ["pyoverkiz==1.8.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -29,8 +29,6 @@ ATTR_NOTIFICATION_ID: Final = "notification_id"
|
||||
ATTR_TITLE: Final = "title"
|
||||
ATTR_STATUS: Final = "status"
|
||||
|
||||
STATUS_UNREAD = "unread"
|
||||
STATUS_READ = "read"
|
||||
|
||||
# Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9
|
||||
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated"
|
||||
@@ -43,7 +41,6 @@ class Notification(TypedDict):
|
||||
message: str
|
||||
notification_id: str
|
||||
title: str | None
|
||||
status: str
|
||||
|
||||
|
||||
class UpdateType(StrEnum):
|
||||
@@ -98,7 +95,6 @@ def async_create(
|
||||
notifications[notification_id] = {
|
||||
ATTR_MESSAGE: message,
|
||||
ATTR_NOTIFICATION_ID: notification_id,
|
||||
ATTR_STATUS: STATUS_UNREAD,
|
||||
ATTR_TITLE: title,
|
||||
ATTR_CREATED_AT: dt_util.utcnow(),
|
||||
}
|
||||
@@ -135,7 +131,6 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the persistent notification component."""
|
||||
notifications = _async_get_or_create_notifications(hass)
|
||||
|
||||
@callback
|
||||
def create_service(call: ServiceCall) -> None:
|
||||
@@ -152,29 +147,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Handle the dismiss notification service call."""
|
||||
async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID])
|
||||
|
||||
@callback
|
||||
def mark_read_service(call: ServiceCall) -> None:
|
||||
"""Handle the mark_read notification service call."""
|
||||
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
|
||||
if notification_id not in notifications:
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Marking persistent_notification read failed: "
|
||||
"Notification ID %s not found"
|
||||
),
|
||||
notification_id,
|
||||
)
|
||||
return
|
||||
|
||||
notification = notifications[notification_id]
|
||||
notification[ATTR_STATUS] = STATUS_READ
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
|
||||
UpdateType.UPDATED,
|
||||
{notification_id: notification},
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"create",
|
||||
@@ -192,10 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_get_notifications)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_notifications)
|
||||
|
||||
|
||||
@@ -33,15 +33,3 @@ dismiss:
|
||||
example: 1234
|
||||
selector:
|
||||
text:
|
||||
|
||||
mark_read:
|
||||
name: Mark read
|
||||
description: Mark a notification read.
|
||||
fields:
|
||||
notification_id:
|
||||
name: Notification ID
|
||||
description: Target ID of the notification, which should be mark read.
|
||||
required: true
|
||||
example: 1234
|
||||
selector:
|
||||
text:
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pulsectl==20.2.4"]
|
||||
"requirements": ["pulsectl==23.5.2"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["regenmaschine"],
|
||||
"requirements": ["regenmaschine==2023.05.1"],
|
||||
"requirements": ["regenmaschine==2023.06.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rapt_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["rapt-ble==0.1.1"]
|
||||
"requirements": ["rapt-ble==0.1.2"]
|
||||
}
|
||||
|
||||
38
homeassistant/components/roborock/diagnostics.py
Normal file
38
homeassistant/components/roborock/diagnostics.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Support for the Airzone diagnostics."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
|
||||
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
|
||||
|
||||
TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
|
||||
"coordinators": {
|
||||
f"**REDACTED-{i}**": {
|
||||
"roborock_device_info": async_redact_data(
|
||||
coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD
|
||||
),
|
||||
"api": coordinator.api.diagnostic_data,
|
||||
}
|
||||
for i, coordinator in enumerate(coordinators.values())
|
||||
},
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["roborock"],
|
||||
"requirements": ["python-roborock==0.17.0"]
|
||||
"requirements": ["python-roborock==0.23.4"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Roborock Models."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
|
||||
from roborock.roborock_typing import DeviceProp
|
||||
@@ -13,3 +14,12 @@ class RoborockHassDeviceInfo:
|
||||
network_info: NetworkInfo
|
||||
product: HomeDataProduct
|
||||
props: DeviceProp
|
||||
|
||||
def as_dict(self) -> dict[str, dict[str, Any]]:
|
||||
"""Turn RoborockHassDeviceInfo into a dictionary."""
|
||||
return {
|
||||
"device": self.device.as_dict(),
|
||||
"network_info": self.network_info.as_dict(),
|
||||
"product": self.product.as_dict(),
|
||||
"props": self.props.as_dict(),
|
||||
}
|
||||
|
||||
@@ -90,8 +90,11 @@
|
||||
"name": "Mop intensity",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"low": "Low",
|
||||
"mild": "Mild",
|
||||
"medium": "Medium",
|
||||
"moderate": "Moderate",
|
||||
"high": "High",
|
||||
"intense": "Intense",
|
||||
"custom": "Custom"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Support for Roborock switch."""
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
@@ -30,6 +32,8 @@ class RoborockSwitchDescriptionMixin:
|
||||
evaluate_value: Callable[[dict], bool]
|
||||
# Sets the status of the switch
|
||||
set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]]
|
||||
# Check support of this feature
|
||||
check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -45,6 +49,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
|
||||
RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0}
|
||||
),
|
||||
get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS),
|
||||
check_support=lambda data: data.api.send_command(
|
||||
RoborockCommand.GET_CHILD_LOCK_STATUS
|
||||
),
|
||||
evaluate_value=lambda data: data["lock_status"] == 1,
|
||||
key="child_lock",
|
||||
translation_key="child_lock",
|
||||
@@ -56,6 +63,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
|
||||
RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0}
|
||||
),
|
||||
get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS),
|
||||
check_support=lambda data: data.api.send_command(
|
||||
RoborockCommand.GET_FLOW_LED_STATUS
|
||||
),
|
||||
evaluate_value=lambda data: data["status"] == 1,
|
||||
key="status_indicator",
|
||||
translation_key="status_indicator",
|
||||
@@ -75,16 +85,38 @@ async def async_setup_entry(
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
async_add_entities(
|
||||
(
|
||||
RoborockSwitchEntity(
|
||||
f"{description.key}_{slugify(device_id)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
possible_entities: list[
|
||||
tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription]
|
||||
] = [
|
||||
(device_id, coordinator, description)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
]
|
||||
# We need to check if this function is supported by the device.
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
description.check_support(coordinator)
|
||||
for _, coordinator, description in possible_entities
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
valid_entities: list[RoborockSwitchEntity] = []
|
||||
for posible_entity, result in zip(possible_entities, results):
|
||||
if isinstance(result, Exception):
|
||||
if not isinstance(result, RoborockException):
|
||||
raise result
|
||||
_LOGGER.debug("Not adding entity because of %s", result)
|
||||
else:
|
||||
valid_entities.append(
|
||||
RoborockSwitchEntity(
|
||||
f"{posible_entity[2].key}_{slugify(posible_entity[0])}",
|
||||
posible_entity[1],
|
||||
posible_entity[2],
|
||||
result,
|
||||
)
|
||||
)
|
||||
async_add_entities(
|
||||
valid_entities,
|
||||
True,
|
||||
)
|
||||
|
||||
@@ -99,10 +131,12 @@ class RoborockSwitchEntity(RoborockEntity, SwitchEntity):
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
entity_description: RoborockSwitchDescription,
|
||||
initial_value: bool,
|
||||
) -> None:
|
||||
"""Create a switch entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(unique_id, coordinator.device_info, coordinator.api)
|
||||
self._attr_is_on = initial_value
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
|
||||
@@ -22,13 +22,11 @@ PLATFORMS = [
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Roku from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
|
||||
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
@@ -36,7 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["russound_rio"],
|
||||
"requirements": ["russound_rio==0.1.8"]
|
||||
"requirements": ["russound-rio==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -535,7 +535,10 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the coordinator."""
|
||||
if self.device.connected:
|
||||
await async_stop_scanner(self.device)
|
||||
try:
|
||||
await async_stop_scanner(self.device)
|
||||
except InvalidAuthError:
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
await self.device.shutdown()
|
||||
await self._async_disconnected()
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sisyphus",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sisyphus_control"],
|
||||
"requirements": ["sisyphus-control==3.1.2"]
|
||||
"requirements": ["sisyphus-control==3.1.3"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.1.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.2.0", "pyroute2==0.7.5"],
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
|
||||
last_stats = await get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 1, statistic_id, True, {}
|
||||
get_last_statistics, self.hass, 1, statistic_id, True, set()
|
||||
)
|
||||
|
||||
if not last_stats:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user