Compare commits

..

20 Commits

Author SHA1 Message Date
Daniel Hjelseth Høyer
205bc0456f Merge branch 'dev' into homevolt 2026-01-20 16:24:32 +01:00
Daniel Hjelseth Høyer
5aa32491c8 Merge branch 'dev' into homevolt 2026-01-15 16:25:46 +01:00
Daniel Hjelseth Høyer
dc2cd2246b Merge branch 'dev' into homevolt 2026-01-15 07:06:51 +01:00
Daniel Hjelseth Høyer
181037820b Merge branch 'dev' into homevolt 2026-01-14 21:05:45 +01:00
Daniel Hjelseth Høyer
6cf15bf70c homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 19:09:37 +01:00
Daniel Hjelseth Høyer
5a34c31e42 Merge branch 'dev' into homevolt 2026-01-14 18:30:20 +01:00
Daniel Hjelseth Høyer
9dcc86f12e homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 18:03:21 +01:00
Daniel Hjelseth Høyer
04429a6eef homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 17:40:51 +01:00
Daniel Hjelseth Høyer
51e2506afb homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 16:41:08 +01:00
Daniel Hjelseth Høyer
e49e5c7c40 Merge branch 'dev' into homevolt 2026-01-14 14:41:26 +01:00
Daniel Hjelseth Høyer
b8dfc523da homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 14:36:43 +01:00
Daniel Hjelseth Høyer
a25fbf57ef Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 17:20:27 +01:00
Daniel Hjelseth Høyer
dac22002b0 Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 14:53:07 +01:00
Daniel Hjelseth Høyer
e61f00a3ae Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 14:15:56 +01:00
Daniel Hjelseth Høyer
14a67c6b5d Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:46:49 +01:00
Daniel Hjelseth Høyer
90ae81f02b Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:39:46 +01:00
Daniel Hjelseth Høyer
a741f214da Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:35:53 +01:00
Daniel Hjelseth Høyer
21d0bd3ce2 Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:22:32 +01:00
Daniel Hjelseth Høyer
d9c1f4850a Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:09:50 +01:00
Daniel Hjelseth Høyer
335994af7e Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 09:44:06 +01:00
166 changed files with 3076 additions and 2854 deletions

View File

@@ -1187,8 +1187,6 @@ jobs:
- pytest-postgres
- pytest-mariadb
timeout-minutes: 10
permissions:
id-token: write
# codecov/test-results-action currently doesn't support tokenless uploads
# therefore we can't run it on forks
if: |
@@ -1200,9 +1198,8 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
with:
report_type: test_results
fail_ci_if_error: true
verbose: true
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.13
rev: v0.13.0
hooks:
- id: ruff-check
args:

2
CODEOWNERS generated
View File

@@ -711,6 +711,8 @@ build.json @home-assistant/supervisor
/tests/components/homematic/ @pvizeli
/homeassistant/components/homematicip_cloud/ @hahn-th
/tests/components/homematicip_cloud/ @hahn-th
/homeassistant/components/homevolt/ @danielhiversen
/tests/components/homevolt/ @danielhiversen
/homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer

View File

@@ -14,7 +14,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed"
"name": "If an alarm is armed"
},
"is_armed_away": {
"description": "Tests if one or more alarms are armed in away mode.",
@@ -24,7 +24,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed away"
"name": "If an alarm is armed away"
},
"is_armed_home": {
"description": "Tests if one or more alarms are armed in home mode.",
@@ -34,7 +34,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed home"
"name": "If an alarm is armed home"
},
"is_armed_night": {
"description": "Tests if one or more alarms are armed in night mode.",
@@ -44,7 +44,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed night"
"name": "If an alarm is armed night"
},
"is_armed_vacation": {
"description": "Tests if one or more alarms are armed in vacation mode.",
@@ -54,7 +54,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed vacation"
"name": "If an alarm is armed vacation"
},
"is_disarmed": {
"description": "Tests if one or more alarms are disarmed.",
@@ -64,7 +64,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is disarmed"
"name": "If an alarm is disarmed"
},
"is_triggered": {
"description": "Tests if one or more alarms are triggered.",
@@ -74,7 +74,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is triggered"
"name": "If an alarm is triggered"
}
},
"device_automation": {

View File

@@ -5,14 +5,9 @@ from __future__ import annotations
import asyncio
import logging
from random import randrange
import sys
from typing import Any, cast
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -29,7 +24,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -43,6 +42,18 @@ from .const import (
SIGNAL_DISCONNECTED,
)
if sys.version_info < (3, 14):
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
else:
class DeviceListener:
"""Dummy class."""
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME_TV = "Apple TV"
@@ -53,25 +64,30 @@ BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
if sys.version_info < (3, 14):
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
else:
AUTH_EXCEPTIONS = ()
CONNECTION_TIMEOUT_EXCEPTIONS = ()
DEVICE_EXCEPTIONS = ()
type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
@@ -79,6 +95,10 @@ type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
"""Set up a config entry for Apple TV."""
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Apple TV is not supported on Python 3.14. Please use Python 3.13."
)
manager = AppleTVManager(hass, entry)
if manager.is_on:

View File

@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.17.0"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@@ -239,15 +239,6 @@ class AppleTvMediaPlayer(
"""
self.async_write_ha_state()
@callback
def volume_device_update(
self, output_device: OutputDevice, old_level: float, new_level: float
) -> None:
"""Output device volume was updated.
This is a callback function from pyatv.interface.AudioListener.
"""
@callback
def outputdevices_update(
self, old_devices: list[OutputDevice], new_devices: list[OutputDevice]

View File

@@ -2,35 +2,14 @@
from __future__ import annotations
import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import ArveConfigEntry, ArveCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_migrate_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version == 1:
# 1 -> 1.2: Unique ID from integer to string
if entry.minor_version == 1:
minor_version = 2
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.unique_id), minor_version=minor_version
)
_LOGGER.debug("Migration successful")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool:
"""Set up Arve from a config entry."""

View File

@@ -19,9 +19,6 @@ _LOGGER = logging.getLogger(__name__)
class ArveConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Arve."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -38,7 +35,7 @@ class ArveConfigFlowHandler(ConfigFlow, domain=DOMAIN):
except ArveConnectionError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(str(customer.customerId))
await self.async_set_unique_id(customer.customerId)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Arve",

View File

@@ -14,7 +14,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is idle"
"name": "If a satellite is idle"
},
"is_listening": {
"description": "Tests if one or more Assist satellites are listening.",
@@ -24,7 +24,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is listening"
"name": "If a satellite is listening"
},
"is_processing": {
"description": "Tests if one or more Assist satellites are processing.",
@@ -34,7 +34,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is processing"
"name": "If a satellite is processing"
},
"is_responding": {
"description": "Tests if one or more Assist satellites are responding.",
@@ -44,7 +44,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is responding"
"name": "If a satellite is responding"
}
},
"entity_component": {

View File

@@ -56,7 +56,7 @@ from homeassistant.core import (
valid_entity_id,
)
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
from homeassistant.helpers import condition as condition_helper, config_validation as cv
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.issue_registry import (
@@ -554,7 +554,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
automation_id: str | None,
name: str,
trigger_config: list[ConfigType],
condition: IfAction | None,
cond_func: IfAction | None,
action_script: Script,
initial_state: bool | None,
variables: ScriptVariables | None,
@@ -567,7 +567,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
self._attr_name = name
self._trigger_config = trigger_config
self._async_detach_triggers: CALLBACK_TYPE | None = None
self._condition = condition
self._cond_func = cond_func
self.action_script = action_script
self.action_script.change_listener = self.async_write_ha_state
self._initial_state = initial_state
@@ -602,12 +602,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced labels."""
referenced = self.action_script.referenced_labels
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_targets(
conf, ATTR_LABEL_ID
)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
return referenced
@@ -617,12 +611,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced floors."""
referenced = self.action_script.referenced_floors
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_targets(
conf, ATTR_FLOOR_ID
)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
return referenced
@@ -632,10 +620,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced areas."""
referenced = self.action_script.referenced_areas
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_targets(conf, ATTR_AREA_ID)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
return referenced
@@ -652,9 +636,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced devices."""
referenced = self.action_script.referenced_devices
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_devices(conf)
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_devices(conf)
for conf in self._trigger_config:
referenced |= set(_trigger_extract_devices(conf))
@@ -666,9 +650,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced entities."""
referenced = self.action_script.referenced_entities
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_entities(conf)
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_entities(conf)
for conf in self._trigger_config:
for entity_id in _trigger_extract_entities(conf):
@@ -788,8 +772,8 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
if (
not skip_condition
and self._condition is not None
and not self._condition(variables)
and self._cond_func is not None
and not self._cond_func(variables)
):
self._logger.debug(
"Conditions not met, aborting automation. Condition summary: %s",
@@ -1051,12 +1035,12 @@ async def _create_automation_entities(
)
if CONF_CONDITIONS in config_block:
condition = await _async_process_if(hass, name, config_block)
cond_func = await _async_process_if(hass, name, config_block)
if condition is None:
if cond_func is None:
continue
else:
condition = None
cond_func = None
# Add trigger variables to variables
variables = None
@@ -1074,7 +1058,7 @@ async def _create_automation_entities(
automation_id,
name,
config_block[CONF_TRIGGERS],
condition,
cond_func,
action_script,
initial_state,
variables,
@@ -1216,7 +1200,7 @@ async def _async_process_if(
if_configs = config[CONF_CONDITIONS]
try:
if_action = await condition_helper.async_conditions_from_config(
if_action = await condition.async_conditions_from_config(
hass, if_configs, LOGGER, name
)
except HomeAssistantError as ex:

View File

@@ -1,6 +1,6 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted thermostats to trigger on.",
"trigger_behavior_description": "The behavior of the targeted climates to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
@@ -298,22 +298,22 @@
"name": "Set target temperature"
},
"toggle": {
"description": "Toggles thermostat, from on to off, or off to on.",
"description": "Toggles climate device, from on to off, or off to on.",
"name": "[%key:common::action::toggle%]"
},
"turn_off": {
"description": "Turns thermostat off.",
"description": "Turns climate device off.",
"name": "[%key:common::action::turn_off%]"
},
"turn_on": {
"description": "Turns thermostat on.",
"description": "Turns climate device on.",
"name": "[%key:common::action::turn_on%]"
}
},
"title": "Climate",
"triggers": {
"current_humidity_changed": {
"description": "Triggers after the humidity measured by one or more thermostats changes.",
"description": "Triggers after the humidity measured by one or more climate-control devices changes.",
"fields": {
"above": {
"description": "Trigger when the humidity is above this value.",
@@ -324,10 +324,10 @@
"name": "Below"
}
},
"name": "Thermostat current humidity changed"
"name": "Climate-control device current humidity changed"
},
"current_humidity_crossed_threshold": {
"description": "Triggers after the humidity measured by one or more thermostats crosses a threshold.",
"description": "Triggers after the humidity measured by one or more climate-control devices crosses a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -346,10 +346,10 @@
"name": "Upper threshold"
}
},
"name": "Thermostat current humidity crossed threshold"
"name": "Climate-control device current humidity crossed threshold"
},
"current_temperature_changed": {
"description": "Triggers after the temperature measured by one or more thermostats changes.",
"description": "Triggers after the temperature measured by one or more climate-control devices changes.",
"fields": {
"above": {
"description": "Trigger when the temperature is above this value.",
@@ -360,10 +360,10 @@
"name": "Below"
}
},
"name": "Thermostat current temperature changed"
"name": "Climate-control device current temperature changed"
},
"current_temperature_crossed_threshold": {
"description": "Triggers after the temperature measured by one or more thermostats crosses a threshold.",
"description": "Triggers after the temperature measured by one or more climate-control devices crosses a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -382,10 +382,10 @@
"name": "Upper threshold"
}
},
"name": "Thermostat current temperature crossed threshold"
"name": "Climate-control device current temperature crossed threshold"
},
"hvac_mode_changed": {
"description": "Triggers after the mode of one or more thermostats changes.",
"description": "Triggers after the mode of one or more climate-control devices changes.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -396,40 +396,40 @@
"name": "Modes"
}
},
"name": "Thermostat mode changed"
"name": "Climate-control device mode changed"
},
"started_cooling": {
"description": "Triggers after one or more thermostats start cooling.",
"description": "Triggers after one or more climate-control devices start cooling.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Thermostat started cooling"
"name": "Climate-control device started cooling"
},
"started_drying": {
"description": "Triggers after one or more thermostats start drying.",
"description": "Triggers after one or more climate-control devices start drying.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Thermostat started drying"
"name": "Climate-control device started drying"
},
"started_heating": {
"description": "Triggers after one or more thermostats start heating.",
"description": "Triggers after one or more climate-control devices start heating.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Thermostat started heating"
"name": "Climate-control device started heating"
},
"target_humidity_changed": {
"description": "Triggers after the humidity setpoint of one or more thermostats changes.",
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
"fields": {
"above": {
"description": "Trigger when the target humidity is above this value.",
@@ -440,10 +440,10 @@
"name": "Below"
}
},
"name": "Thermostat target humidity changed"
"name": "Climate-control device target humidity changed"
},
"target_humidity_crossed_threshold": {
"description": "Triggers after the humidity setpoint of one or more thermostats crosses a threshold.",
"description": "Triggers after the humidity setpoint of one or more climate-control devices crosses a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -462,10 +462,10 @@
"name": "Upper threshold"
}
},
"name": "Thermostat target humidity crossed threshold"
"name": "Climate-control device target humidity crossed threshold"
},
"target_temperature_changed": {
"description": "Triggers after the temperature setpoint of one or more thermostats changes.",
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
"fields": {
"above": {
"description": "Trigger when the target temperature is above this value.",
@@ -476,10 +476,10 @@
"name": "Below"
}
},
"name": "Thermostat target temperature changed"
"name": "Climate-control device target temperature changed"
},
"target_temperature_crossed_threshold": {
"description": "Triggers after the temperature setpoint of one or more thermostats crosses a threshold.",
"description": "Triggers after the temperature setpoint of one or more climate-control devices crosses a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -498,27 +498,27 @@
"name": "Upper threshold"
}
},
"name": "Thermostat target temperature crossed threshold"
"name": "Climate-control device target temperature crossed threshold"
},
"turned_off": {
"description": "Triggers after one or more thermostats turn off.",
"description": "Triggers after one or more climate-control devices turn off.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Thermostat turned off"
"name": "Climate-control device turned off"
},
"turned_on": {
"description": "Triggers after one or more thermostats turn on, regardless of the mode.",
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Thermostat turned on"
"name": "Climate-control device turned on"
}
}
}

View File

@@ -14,7 +14,7 @@
"name": "[%key:component::fan::common::condition_behavior_name%]"
}
},
"name": "Fan is off"
"name": "If a fan is off"
},
"is_on": {
"description": "Tests if one or more fans are on.",
@@ -24,7 +24,7 @@
"name": "[%key:component::fan::common::condition_behavior_name%]"
}
},
"name": "Fan is on"
"name": "If a fan is on"
}
},
"device_automation": {

View File

@@ -7,11 +7,20 @@
"benzene": {
"default": "mdi:molecule"
},
"nitrogen_dioxide": {
"default": "mdi:molecule"
},
"nitrogen_monoxide": {
"default": "mdi:molecule"
},
"non_methane_hydrocarbons": {
"default": "mdi:molecule"
},
"ozone": {
"default": "mdi:molecule"
},
"sulphur_dioxide": {
"default": "mdi:molecule"
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==3.0.0"]
"requirements": ["google_air_quality_api==2.1.2"]
}

View File

@@ -13,11 +13,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
CONF_LATITUDE,
CONF_LONGITUDE,
)
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -118,7 +114,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
exists_fn=lambda x: "co" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.co.concentration.value,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
AirQualitySensorEntityDescription(
key="nh3",
@@ -146,16 +141,16 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
),
AirQualitySensorEntityDescription(
key="no2",
translation_key="nitrogen_dioxide",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement_fn=lambda x: x.pollutants.no2.concentration.units,
exists_fn=lambda x: "no2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.no2.concentration.value,
),
AirQualitySensorEntityDescription(
key="o3",
translation_key="ozone",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement_fn=lambda x: x.pollutants.o3.concentration.units,
exists_fn=lambda x: "o3" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.o3.concentration.value,
@@ -178,8 +173,8 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
),
AirQualitySensorEntityDescription(
key="so2",
translation_key="sulphur_dioxide",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement_fn=lambda x: x.pollutants.so2.concentration.units,
exists_fn=lambda x: "so2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.so2.concentration.value,

View File

@@ -205,12 +205,21 @@
"so2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
}
},
"nitrogen_dioxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
},
"nitrogen_monoxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]"
},
"non_methane_hydrocarbons": {
"name": "Non-methane hydrocarbons"
},
"ozone": {
"name": "[%key:component::sensor::entity_component::ozone::name%]"
},
"sulphur_dioxide": {
"name": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
},
"uaqi": {
"name": "Universal Air Quality Index"
},

View File

@@ -83,9 +83,6 @@
"invalid_credentials": "Input is incomplete. You must provide either your login details or an API token",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"advanced": {
"data": {

View File

@@ -6,7 +6,7 @@ from typing import Any
from aiohttp import web
from homeassistant.components import frontend
from homeassistant.components import frontend, panel_custom
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ATTR_ICON
from homeassistant.core import HomeAssistant
@@ -33,7 +33,7 @@ async def async_setup_addon_panel(hass: HomeAssistant, hassio: HassIO) -> None:
# _register_panel never suspends and is only
# a coroutine because it would be a breaking change
# to make it a normal function
_register_panel(hass, addon, data)
await _register_panel(hass, addon, data)
class HassIOAddonPanel(HomeAssistantView):
@@ -58,7 +58,7 @@ class HassIOAddonPanel(HomeAssistantView):
data = panels[addon]
# Register panel
_register_panel(self.hass, addon, data)
await _register_panel(self.hass, addon, data)
return web.Response()
async def delete(self, request: web.Request, addon: str) -> web.Response:
@@ -76,14 +76,18 @@ class HassIOAddonPanel(HomeAssistantView):
return {}
def _register_panel(hass: HomeAssistant, addon: str, data: dict[str, Any]):
async def _register_panel(
hass: HomeAssistant, addon: str, data: dict[str, Any]
) -> None:
"""Init coroutine to register the panel."""
frontend.async_register_built_in_panel(
await panel_custom.async_register_panel(
hass,
"app",
frontend_url_path=addon,
webcomponent_name="hassio-main",
sidebar_title=data[ATTR_TITLE],
sidebar_icon=data[ATTR_ICON],
js_url="/api/hassio/app/entrypoint.js",
embed_iframe=True,
require_admin=data[ATTR_ADMIN],
config={"addon": addon},
config={"ingress": addon},
)

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["hdfury==1.4.2"]
"requirements": ["hdfury==1.3.1"]
}

View File

@@ -0,0 +1,36 @@
"""The Homevolt integration."""
from __future__ import annotations
from homevolt import Homevolt
from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool:
"""Set up Homevolt from a config entry."""
host: str = entry.data[CONF_HOST]
password: str | None = entry.data.get(CONF_PASSWORD)
websession = async_get_clientsession(hass)
client = Homevolt(host, password, websession=websession)
coordinator = HomevoltDataUpdateCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,70 @@
"""Config flow for the Homevolt integration."""
from __future__ import annotations
import logging
from typing import Any
from homevolt import Homevolt, HomevoltAuthenticationError, HomevoltConnectionError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PASSWORD): str,
}
)
class HomevoltConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Homevolt."""
VERSION = 1
MINOR_VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
password = user_input.get(CONF_PASSWORD)
websession = async_get_clientsession(self.hass)
client = Homevolt(host, password, websession=websession)
try:
await client.update_info()
device = client.get_device()
device_id = device.device_id
except HomevoltAuthenticationError:
errors["base"] = "invalid_auth"
except HomevoltConnectionError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception(
"Error occurred while connecting to the Homevolt battery"
)
errors["base"] = "unknown"
else:
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Homevolt Local",
data={
CONF_HOST: host,
CONF_PASSWORD: password,
},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -0,0 +1,9 @@
"""Constants for the Homevolt integration."""
from __future__ import annotations
from datetime import timedelta
DOMAIN = "homevolt"
MANUFACTURER = "Homevolt"
SCAN_INTERVAL = timedelta(seconds=15)

View File

@@ -0,0 +1,56 @@
"""Data update coordinator for Homevolt integration."""
from __future__ import annotations
import logging
from homevolt import (
Device,
Homevolt,
HomevoltAuthenticationError,
HomevoltConnectionError,
HomevoltError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, SCAN_INTERVAL
type HomevoltConfigEntry = ConfigEntry[HomevoltDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
class HomevoltDataUpdateCoordinator(DataUpdateCoordinator[Device]):
"""Class to manage fetching Homevolt data."""
config_entry: HomevoltConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: HomevoltConfigEntry,
client: Homevolt,
) -> None:
"""Initialize the Homevolt coordinator."""
self.client = client
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
config_entry=entry,
)
async def _async_update_data(self) -> Device:
"""Fetch data from the Homevolt API."""
try:
await self.client.update_info()
return self.client.get_device()
except HomevoltAuthenticationError as err:
raise ConfigEntryAuthFailed from err
except (HomevoltConnectionError, HomevoltError) as err:
raise UpdateFailed(f"Error communicating with device: {err}") from err

View File

@@ -0,0 +1,12 @@
{
"domain": "homevolt",
"name": "Homevolt",
"codeowners": ["@danielhiversen"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/homevolt",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["homevolt==0.2.4"]
}

View File

@@ -0,0 +1,70 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Local_polling without events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register custom actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: Integration does not have an options flow.
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,162 @@
"""Support for Homevolt sensors."""
from __future__ import annotations
from homevolt.models import SensorType
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
PARALLEL_UPDATES = 0 # Coordinator-based updates
SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=SensorType.COUNT,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=SensorType.ENERGY_TOTAL,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
),
SensorEntityDescription(
key=SensorType.ENERGY_INCREASING,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
),
SensorEntityDescription(
key=SensorType.FREQUENCY,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
),
SensorEntityDescription(
key=SensorType.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(
key=SensorType.POWER,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key=SensorType.SCHEDULE_TYPE,
),
SensorEntityDescription(
key=SensorType.SIGNAL_STRENGTH,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
),
SensorEntityDescription(
key=SensorType.TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key=SensorType.TEXT,
),
SensorEntityDescription(
key=SensorType.VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
SensorEntityDescription(
key=SensorType.CURRENT,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HomevoltConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Homevolt sensor."""
coordinator = entry.runtime_data
entities = []
sensors_by_key = {sensor.key: sensor for sensor in SENSORS}
for sensor_key, sensor in coordinator.data.sensors.items():
if (description := sensors_by_key.get(sensor.type)) is None:
continue
entities.append(
HomevoltSensor(
description,
coordinator,
sensor_key,
)
)
async_add_entities(entities)
class HomevoltSensor(CoordinatorEntity[HomevoltDataUpdateCoordinator], SensorEntity):
"""Representation of a Homevolt sensor."""
_attr_has_entity_name = True
def __init__(
self,
description: SensorEntityDescription,
coordinator: HomevoltDataUpdateCoordinator,
sensor_key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
device_id = coordinator.data.device_id
self._attr_unique_id = f"{device_id}_{sensor_key}"
sensor_data = coordinator.data.sensors[sensor_key]
self._attr_translation_key = sensor_data.slug
self._sensor_key = sensor_key
device_metadata = coordinator.data.device_metadata.get(
sensor_data.device_identifier
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{device_id}_{sensor_data.device_identifier}")},
configuration_url=coordinator.client.base_url,
manufacturer=MANUFACTURER,
model=device_metadata.model if device_metadata else None,
name=device_metadata.name if device_metadata else None,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._sensor_key in self.coordinator.data.sensors
@property
def native_value(self) -> StateType:
"""Return the native value of the sensor."""
return self.coordinator.data.sensors[self._sensor_key].value

View File

@@ -0,0 +1,198 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The IP address or hostname of your Homevolt battery on your local network.",
"password": "The local password configured for your Homevolt battery, if required."
},
"description": "Connect Home Assistant to your Homevolt battery over the local network.",
"title": "Homevolt Local"
}
}
},
"entity": {
"sensor": {
"available_charging_energy": {
"name": "Available charging energy"
},
"available_charging_power": {
"name": "Available charging power"
},
"available_discharge_energy": {
"name": "Available discharge energy"
},
"available_discharge_power": {
"name": "Available discharge power"
},
"average_rssi_grid": {
"name": "Grid average RSSI"
},
"average_rssi_load": {
"name": "Load average RSSI"
},
"battery_state_of_charge": {
"name": "Battery state of charge"
},
"charge_cycles": {
"name": "Charge cycles"
},
"energy_exported_grid": {
"name": "Grid exported energy"
},
"energy_exported_load": {
"name": "Load exported energy"
},
"energy_imported_grid": {
"name": "Grid imported energy"
},
"energy_imported_load": {
"name": "Load imported energy"
},
"exported_energy": {
"name": "Exported energy"
},
"frequency": {
"name": "Frequency"
},
"imported_energy": {
"name": "Imported energy"
},
"l1_current": {
"name": "L1 current"
},
"l1_current_grid": {
"name": "Grid L1 current"
},
"l1_current_load": {
"name": "Load L1 current"
},
"l1_l2_voltage": {
"name": "L1-L2 voltage"
},
"l1_power_grid": {
"name": "Grid L1 power"
},
"l1_power_load": {
"name": "Load L1 power"
},
"l1_voltage": {
"name": "L1 voltage"
},
"l1_voltage_grid": {
"name": "Grid L1 voltage"
},
"l1_voltage_load": {
"name": "Load L1 voltage"
},
"l2_current": {
"name": "L2 current"
},
"l2_current_grid": {
"name": "Grid L2 current"
},
"l2_current_load": {
"name": "Load L2 current"
},
"l2_l3_voltage": {
"name": "L2-L3 voltage"
},
"l2_power_grid": {
"name": "Grid L2 power"
},
"l2_power_load": {
"name": "Load L2 power"
},
"l2_voltage": {
"name": "L2 voltage"
},
"l2_voltage_grid": {
"name": "Grid L2 voltage"
},
"l2_voltage_load": {
"name": "Load L2 voltage"
},
"l3_current": {
"name": "L3 current"
},
"l3_current_grid": {
"name": "Grid L3 current"
},
"l3_current_load": {
"name": "Load L3 current"
},
"l3_l1_voltage": {
"name": "L3-L1 voltage"
},
"l3_power_grid": {
"name": "Grid L3 power"
},
"l3_power_load": {
"name": "Load L3 power"
},
"l3_voltage": {
"name": "L3 voltage"
},
"l3_voltage_grid": {
"name": "Grid L3 voltage"
},
"l3_voltage_load": {
"name": "Load L3 voltage"
},
"power": {
"name": "Power"
},
"power_grid": {
"name": "Grid power"
},
"power_load": {
"name": "Load power"
},
"rssi_grid": {
"name": "Grid RSSI"
},
"rssi_load": {
"name": "Load RSSI"
},
"schedule_id": {
"name": "Schedule ID"
},
"schedule_max_discharge": {
"name": "Schedule max discharge"
},
"schedule_max_power": {
"name": "Schedule max power"
},
"schedule_power_setpoint": {
"name": "Schedule power setpoint"
},
"schedule_type": {
"name": "Schedule type"
},
"state_of_charge": {
"name": "State of charge"
},
"system_temperature": {
"name": "System temperature"
},
"tmax": {
"name": "Maximum temperature"
},
"tmin": {
"name": "Minimum temperature"
}
}
}
}

View File

@@ -28,7 +28,6 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
from .const import DOMAIN, UPDATE_INTERVAL
from .entity import AqualinkEntity
@@ -67,11 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) ->
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
aqualink = AqualinkClient(
username,
password,
httpx_client=get_async_client(hass, alpn_protocols=SSL_ALPN_HTTP11_HTTP2),
)
aqualink = AqualinkClient(username, password, httpx_client=get_async_client(hass))
try:
await aqualink.login()
except AqualinkServiceException as login_exception:

View File

@@ -15,7 +15,6 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
from .const import DOMAIN
@@ -37,11 +36,7 @@ class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):
try:
async with AqualinkClient(
username,
password,
httpx_client=get_async_client(
self.hass, alpn_protocols=SSL_ALPN_HTTP11_HTTP2
),
username, password, httpx_client=get_async_client(self.hass)
):
pass
except AqualinkServiceUnauthorizedException:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==2.0.1"]
"requirements": ["imgw_pib==1.6.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["kostal"],
"requirements": ["pykoplenti==1.5.0"]
"requirements": ["pykoplenti==1.3.0"]
}

View File

@@ -49,7 +49,7 @@
"name": "[%key:component::light::common::condition_behavior_name%]"
}
},
"name": "Light is off"
"name": "If a light is off"
},
"is_on": {
"description": "Tests if one or more lights are on.",
@@ -59,7 +59,7 @@
"name": "[%key:component::light::common::condition_behavior_name%]"
}
},
"name": "Light is on"
"name": "If a light is on"
}
},
"device_automation": {

View File

@@ -1,47 +1,24 @@
"""Provides triggers for lights."""
from typing import Any
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import (
EntityNumericalStateAttributeChangedTriggerBase,
EntityNumericalStateAttributeCrossedThresholdTriggerBase,
Trigger,
make_entity_numerical_state_attribute_changed_trigger,
make_entity_numerical_state_attribute_crossed_threshold_trigger,
make_entity_target_state_trigger,
)
from . import ATTR_BRIGHTNESS
from .const import DOMAIN
def _convert_uint8_to_percentage(value: Any) -> float:
"""Convert a uint8 value (0-255) to a percentage (0-100)."""
return (float(value) / 255.0) * 100.0
class BrightnessChangedTrigger(EntityNumericalStateAttributeChangedTriggerBase):
"""Trigger for brightness changed."""
_domain = DOMAIN
_attribute = ATTR_BRIGHTNESS
_converter = staticmethod(_convert_uint8_to_percentage)
class BrightnessCrossedThresholdTrigger(
EntityNumericalStateAttributeCrossedThresholdTriggerBase
):
"""Trigger for brightness crossed threshold."""
_domain = DOMAIN
_attribute = ATTR_BRIGHTNESS
_converter = staticmethod(_convert_uint8_to_percentage)
TRIGGERS: dict[str, type[Trigger]] = {
"brightness_changed": BrightnessChangedTrigger,
"brightness_crossed_threshold": BrightnessCrossedThresholdTrigger,
"brightness_changed": make_entity_numerical_state_attribute_changed_trigger(
DOMAIN, ATTR_BRIGHTNESS
),
"brightness_crossed_threshold": make_entity_numerical_state_attribute_crossed_threshold_trigger(
DOMAIN, ATTR_BRIGHTNESS
),
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
}

View File

@@ -22,10 +22,7 @@
number:
selector:
number:
max: 100
min: 0
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:

View File

@@ -2,7 +2,6 @@
from dataclasses import dataclass
from http import HTTPStatus
import logging
import aiohttp
from microBeesPy import MicroBees
@@ -16,8 +15,6 @@ from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN, PLATFORMS
from .coordinator import MicroBeesUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class HomeAssistantMicroBeesData:
@@ -28,23 +25,6 @@ class HomeAssistantMicroBeesData:
session: config_entry_oauth2_flow.OAuth2Session
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version == 1:
# 1 -> 1.2: Unique ID from integer to string
if entry.minor_version == 1:
minor_version = 2
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.unique_id), minor_version=minor_version
)
_LOGGER.debug("Migration successful")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up microBees from a config entry."""
implementation = (

View File

@@ -19,8 +19,6 @@ class OAuth2FlowHandler(
"""Handle a config flow for microBees."""
DOMAIN = DOMAIN
VERSION = 1
MINOR_VERSION = 2
@property
def logger(self) -> logging.Logger:
@@ -49,7 +47,7 @@ class OAuth2FlowHandler(
self.logger.exception("Unexpected error")
return self.async_abort(reason="unknown")
await self.async_set_unique_id(str(current_user.id))
await self.async_set_unique_id(current_user.id)
if self.source != SOURCE_REAUTH:
self._abort_if_unique_id_configured()
return self.async_create_entry(

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -17,28 +15,9 @@ from .api import AuthenticatedMonzoAPI
from .const import DOMAIN
from .coordinator import MonzoCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version == 1:
# 1 -> 1.2: Unique ID from integer to string
if entry.minor_version == 1:
minor_version = 2
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.unique_id), minor_version=minor_version
)
_LOGGER.debug("Migration successful")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Monzo from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)

View File

@@ -21,8 +21,6 @@ class MonzoFlowHandler(
"""Handle a config flow."""
DOMAIN = DOMAIN
VERSION = 1
MINOR_VERSION = 2
oauth_data: dict[str, Any]
@@ -53,7 +51,7 @@ class MonzoFlowHandler(
"""Create an entry for the flow."""
self.oauth_data = data
user_id = data[CONF_TOKEN]["user_id"]
await self.async_set_unique_id(str(user_id))
await self.async_set_unique_id(user_id)
if self.source != SOURCE_REAUTH:
self._abort_if_unique_id_configured()
else:

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aionfty"],
"quality_scale": "platinum",
"requirements": ["aiontfy==0.7.0"]
"requirements": ["aiontfy==0.6.1"]
}

View File

@@ -43,7 +43,6 @@ ATTR_ICON = "icon"
ATTR_MARKDOWN = "markdown"
ATTR_PRIORITY = "priority"
ATTR_TAGS = "tags"
ATTR_SEQUENCE_ID = "sequence_id"
SERVICE_PUBLISH_SCHEMA = cv.make_entity_service_schema(
{
@@ -61,7 +60,6 @@ SERVICE_PUBLISH_SCHEMA = cv.make_entity_service_schema(
vol.Optional(ATTR_EMAIL): vol.Email(),
vol.Optional(ATTR_CALL): cv.string,
vol.Optional(ATTR_ICON): vol.All(vol.Url(), vol.Coerce(URL)),
vol.Optional(ATTR_SEQUENCE_ID): cv.string,
}
)

View File

@@ -88,8 +88,3 @@ publish:
type: url
autocomplete: url
example: https://example.org/logo.png
sequence_id:
required: false
selector:
text:
example: "Mc3otamDNcpJ"

View File

@@ -1,7 +1,6 @@
{
"common": {
"add_topic_description": "Set up a topic for notifications.",
"sequence_id": "Sequence ID",
"topic": "Topic"
},
"config": {
@@ -172,9 +171,6 @@
"icon": { "name": "Icon" },
"message": { "name": "Message" },
"priority": { "name": "Priority" },
"sequence_id": {
"name": "[%key:component::ntfy::common::sequence_id%]"
},
"tags": { "name": "Tags" },
"time": { "name": "Time" },
"title": { "name": "Title" },
@@ -360,10 +356,6 @@
"description": "All messages have a priority that defines how urgently your phone notifies you, depending on the configured vibration patterns, notification sounds, and visibility in the notification drawer or pop-over.",
"name": "Message priority"
},
"sequence_id": {
"description": "Enter a message or sequence ID to update an existing notification, or specify a sequence ID to reference later when updating, clearing (mark as read and dismiss), or deleting a notification.",
"name": "[%key:component::ntfy::common::sequence_id%]"
},
"tags": {
"description": "Add tags or emojis to the notification. Emojis (using shortcodes like smile) will appear in the notification title or message. Other tags will be displayed below the notification content.",
"name": "Tags/Emojis"

View File

@@ -247,7 +247,7 @@ class NumberDeviceClass(StrEnum):
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `ppb` (parts per billion), `μg/m³`
Unit of measurement: `μg/m³`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
@@ -265,7 +265,7 @@ class NumberDeviceClass(StrEnum):
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `ppb` (parts per billion), `μg/m³`
Unit of measurement: `μg/m³`
"""
PH = "ph"
@@ -517,16 +517,10 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX},
NumberDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
NumberDeviceClass.MOISTURE: {PERCENTAGE},
NumberDeviceClass.NITROGEN_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.OZONE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PH: {None},
NumberDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},

View File

@@ -8,9 +8,6 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"user": {
"data": {

View File

@@ -178,7 +178,6 @@ class OneDriveBackupAgent(BackupAgent):
file,
upload_chunk_size=upload_chunk_size,
session=async_get_clientsession(self._hass),
smart_chunk_size=True,
)
except HashMismatchError as err:
raise BackupAgentError(

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.1"]
"requirements": ["onedrive-personal-sdk==0.1.0"]
}

View File

@@ -25,9 +25,6 @@
"folder_creation_error": "Failed to create folder",
"folder_rename_error": "Failed to rename folder"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"folder_name": {
"data": {

View File

@@ -13,9 +13,6 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"reauth_confirm": {
"data": {

View File

@@ -9,7 +9,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["aioqsw"],
"requirements": ["aioqsw==0.4.2"]

View File

@@ -5,7 +5,6 @@
"codeowners": ["@rabbit-air"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rabbitair",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["python-rabbitair==0.0.8"],
"zeroconf": ["_rabbitair._udp.local."]

View File

@@ -13,7 +13,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/radiotherm",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["radiotherm"],
"requirements": ["radiotherm==2.1.0"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@konikvranik", "@allenporter"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainbird",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyrainbird"],
"requirements": ["pyrainbird==6.0.1"]

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["aioraven==0.7.1"],
"usb": [

View File

@@ -15,7 +15,6 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/rapt_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["rapt-ble==0.1.2"]
}

View File

@@ -7,9 +7,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown_license_plate": "Unknown license plate"
},
"initiate_flow": {
"user": "Add vehicle"
},
"step": {
"user": {
"data": {

View File

@@ -59,8 +59,6 @@ from homeassistant.util.unit_conversion import (
InformationConverter,
MassConverter,
MassVolumeConcentrationConverter,
NitrogenDioxideConcentrationConverter,
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
ReactiveEnergyConverter,
@@ -227,10 +225,8 @@ _PRIMARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
_SECONDARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
CarbonMonoxideConcentrationConverter,
NitrogenDioxideConcentrationConverter,
OzoneConcentrationConverter,
SulphurDioxideConcentrationConverter,
TemperatureDeltaConverter,
SulphurDioxideConcentrationConverter,
]
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {

View File

@@ -33,8 +33,6 @@ from homeassistant.util.unit_conversion import (
InformationConverter,
MassConverter,
MassVolumeConcentrationConverter,
NitrogenDioxideConcentrationConverter,
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
ReactiveEnergyConverter,
@@ -87,14 +85,11 @@ UNIT_SCHEMA = vol.Schema(
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional("duration"): vol.In(DurationConverter.VALID_UNITS),
vol.Optional("electric_current"): vol.In(ElectricCurrentConverter.VALID_UNITS),
vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS),
vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS),
vol.Optional("energy_distance"): vol.In(EnergyDistanceConverter.VALID_UNITS),
vol.Optional("information"): vol.In(InformationConverter.VALID_UNITS),
vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS),
vol.Optional("nitrogen_dioxide"): vol.In(
NitrogenDioxideConcentrationConverter.VALID_UNITS
),
vol.Optional("ozone"): vol.In(OzoneConcentrationConverter.VALID_UNITS),
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),
vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS),
vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS),
@@ -108,7 +103,6 @@ UNIT_SCHEMA = vol.Schema(
TemperatureDeltaConverter.VALID_UNITS
),
vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS),
vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS),
vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS),
vol.Optional("volume_flow_rate"): vol.In(VolumeFlowRateConverter.VALID_UNITS),
}

View File

@@ -4,7 +4,6 @@
"codeowners": ["@ashionky"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/refoss",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["refoss-ha==1.2.5"],
"single_config_entry": true

View File

@@ -10,7 +10,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/rehlko",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aiokem"],
"quality_scale": "silver",

View File

@@ -12,9 +12,6 @@
"invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"kamereon": {
"data": {

View File

@@ -4,7 +4,6 @@
"codeowners": ["@jimmyd-be"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/renson",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["renson-endura-delta==1.7.2"]
}

View File

@@ -4,7 +4,6 @@
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["RFXtrx"],
"requirements": ["pyRFXtrx==0.31.1"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@milanmeu", "@frenck", "@quebulm"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyrituals"],
"requirements": ["pyrituals==0.0.7"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@xeniter"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/romy",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["romy==0.0.10"],
"zeroconf": ["_aicu-http._tcp.local."]

View File

@@ -22,7 +22,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/roomba",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["paho_mqtt", "roombapy"],
"requirements": ["roombapy==1.9.0"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@pavoni"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roon",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["roonapi"],
"requirements": ["roonapi==0.1.6"]

View File

@@ -4,7 +4,6 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rova",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["rova"],
"requirements": ["rova==0.4.1"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@noahhusby"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",

View File

@@ -10,7 +10,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/ruuvi_gateway",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["aioruuvigateway==0.1.0"]
}

View File

@@ -15,7 +15,6 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["ruuvitag-ble==0.4.0"]
}

View File

@@ -4,7 +4,6 @@
"codeowners": ["@OnFreund", "@elad-bar", "@maorcc"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rympro",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["pyrympro==0.0.9"]
}

View File

@@ -4,7 +4,6 @@
"codeowners": ["@shaiu", "@jpbede"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["pysabnzbd"],
"quality_scale": "bronze",

View File

@@ -4,7 +4,6 @@
"codeowners": ["@tomaszsluszniak"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sanix",
"integration_type": "device",
"iot_class": "cloud_polling",
"requirements": ["sanix==1.0.6"]
}

View File

@@ -4,7 +4,6 @@
"codeowners": ["@dknowles2"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/schlage",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["pyschlage==2025.9.0"]
}

View File

@@ -13,7 +13,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["screenlogicpy"],
"requirements": ["screenlogicpy==0.10.2"]

View File

@@ -18,7 +18,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/sense",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.13.8"]

View File

@@ -12,7 +12,6 @@
"homekit": {
"models": ["Sensibo"]
},
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pysensibo"],
"quality_scale": "platinum",

View File

@@ -63,8 +63,6 @@ from homeassistant.util.unit_conversion import (
InformationConverter,
MassConverter,
MassVolumeConcentrationConverter,
NitrogenDioxideConcentrationConverter,
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
ReactiveEnergyConverter,
@@ -285,7 +283,7 @@ class SensorDeviceClass(StrEnum):
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `ppb` (parts per billion), `μg/m³`
Unit of measurement: `μg/m³`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
@@ -303,7 +301,7 @@ class SensorDeviceClass(StrEnum):
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `ppb` (parts per billion),`μg/m³`
Unit of measurement: `μg/m³`
"""
PH = "ph"
@@ -565,8 +563,6 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.ENERGY_DISTANCE: EnergyDistanceConverter,
SensorDeviceClass.ENERGY_STORAGE: EnergyConverter,
SensorDeviceClass.GAS: VolumeConverter,
SensorDeviceClass.NITROGEN_DIOXIDE: NitrogenDioxideConcentrationConverter,
SensorDeviceClass.OZONE: OzoneConcentrationConverter,
SensorDeviceClass.POWER: PowerConverter,
SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter,
SensorDeviceClass.PRECIPITATION: DistanceConverter,
@@ -635,16 +631,10 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX},
SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
SensorDeviceClass.MOISTURE: {PERCENTAGE},
SensorDeviceClass.NITROGEN_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.OZONE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PH: {None},
SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},

View File

@@ -18,9 +18,6 @@
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"pick_implementation": {
"data": {

View File

@@ -49,8 +49,8 @@ DEFAULT_NAME = "Template Select"
SELECT_COMMON_SCHEMA = vol.Schema(
{
vol.Required(ATTR_OPTIONS): cv.template,
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Optional(ATTR_OPTIONS): cv.template,
vol.Optional(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_STATE): cv.template,
}
)

View File

@@ -8,6 +8,7 @@ import logging
import aiohttp
from aiohttp.client_exceptions import ClientError, ClientResponseError
import tibber
from tibber import data_api as tibber_data_api
from homeassistant.const import CONF_ACCESS_TOKEN, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
@@ -22,7 +23,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util, ssl as ssl_util
from .const import AUTH_IMPLEMENTATION, DATA_HASS_CONFIG, DOMAIN, TibberConfigEntry
from .const import (
AUTH_IMPLEMENTATION,
CONF_LEGACY_ACCESS_TOKEN,
DATA_HASS_CONFIG,
DOMAIN,
TibberConfigEntry,
)
from .coordinator import TibberDataAPICoordinator
from .services import async_setup_services
@@ -37,23 +44,24 @@ _LOGGER = logging.getLogger(__name__)
class TibberRuntimeData:
"""Runtime data for Tibber API entries."""
tibber_connection: tibber.Tibber
session: OAuth2Session
data_api_coordinator: TibberDataAPICoordinator | None = field(default=None)
_client: tibber.Tibber | None = None
_client: tibber_data_api.TibberDataAPI | None = None
async def async_get_client(self, hass: HomeAssistant) -> tibber.Tibber:
"""Return an authenticated Tibber client."""
async def async_get_client(
self, hass: HomeAssistant
) -> tibber_data_api.TibberDataAPI:
"""Return an authenticated Tibber Data API client."""
await self.session.async_ensure_token_valid()
token = self.session.token
access_token = token.get(CONF_ACCESS_TOKEN)
if not access_token:
raise ConfigEntryAuthFailed("Access token missing from OAuth session")
if self._client is None:
self._client = tibber.Tibber(
access_token=access_token,
self._client = tibber_data_api.TibberDataAPI(
access_token,
websession=async_get_clientsession(hass),
time_zone=dt_util.get_default_time_zone(),
ssl=ssl_util.get_default_context(),
)
self._client.set_access_token(access_token)
return self._client
@@ -80,6 +88,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: TibberConfigEntry) -> bo
translation_key="data_api_reauth_required",
)
tibber_connection = tibber.Tibber(
access_token=entry.data[CONF_LEGACY_ACCESS_TOKEN],
websession=async_get_clientsession(hass),
time_zone=dt_util.get_default_time_zone(),
ssl=ssl_util.get_default_context(),
)
async def _close(event: Event) -> None:
await tibber_connection.rt_disconnect()
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close))
try:
await tibber_connection.update_info()
except (
TimeoutError,
aiohttp.ClientError,
tibber.RetryableHttpExceptionError,
) as err:
raise ConfigEntryNotReady("Unable to connect") from err
except tibber.InvalidLoginError as exp:
_LOGGER.error("Failed to login. %s", exp)
return False
except tibber.FatalHttpExceptionError:
return False
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
@@ -101,29 +135,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: TibberConfigEntry) -> bo
raise ConfigEntryNotReady from err
entry.runtime_data = TibberRuntimeData(
tibber_connection=tibber_connection,
session=session,
)
tibber_connection = await entry.runtime_data.async_get_client(hass)
async def _close(event: Event) -> None:
await tibber_connection.rt_disconnect()
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close))
try:
await tibber_connection.update_info()
except (
TimeoutError,
aiohttp.ClientError,
tibber.RetryableHttpExceptionError,
) as err:
raise ConfigEntryNotReady("Unable to connect") from err
except tibber.InvalidLoginError as err:
raise ConfigEntryAuthFailed("Invalid login credentials") from err
except tibber.FatalHttpExceptionError as err:
raise ConfigEntryNotReady("Fatal HTTP error from Tibber API") from err
coordinator = TibberDataAPICoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data.data_api_coordinator = coordinator
@@ -139,6 +154,5 @@ async def async_unload_entry(
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
tibber_connection = await config_entry.runtime_data.async_get_client(hass)
await tibber_connection.rt_disconnect()
await config_entry.runtime_data.tibber_connection.rt_disconnect()
return unload_ok

View File

@@ -8,16 +8,21 @@ from typing import Any
import aiohttp
import tibber
from tibber import data_api as tibber_data_api
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .const import DATA_API_DEFAULT_SCOPES, DOMAIN
from .const import CONF_LEGACY_ACCESS_TOKEN, DATA_API_DEFAULT_SCOPES, DOMAIN
DATA_SCHEMA = vol.Schema({vol.Required(CONF_LEGACY_ACCESS_TOKEN): str})
ERR_TIMEOUT = "timeout"
ERR_CLIENT = "cannot_connect"
ERR_TOKEN = "invalid_access_token"
TOKEN_URL = "https://developer.tibber.com/settings/access-token"
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +36,8 @@ class TibberConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
super().__init__()
self._oauth_data: dict[str, Any] | None = None
self._access_token: str | None = None
self._title = ""
@property
def logger(self) -> logging.Logger:
@@ -46,70 +52,114 @@ class TibberConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"scope": " ".join(DATA_API_DEFAULT_SCOPES),
}
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle a reauth flow."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication by reusing the user step."""
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
data_schema = self.add_suggested_values_to_schema(
DATA_SCHEMA, {CONF_LEGACY_ACCESS_TOKEN: self._access_token or ""}
)
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Finalize the OAuth flow and create the config entry."""
self._oauth_data = data
return await self._async_validate_and_create()
return self.async_show_form(
step_id=SOURCE_USER,
data_schema=data_schema,
description_placeholders={"url": TOKEN_URL},
errors={},
)
async def async_step_connection_error(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle connection error retry."""
if user_input is not None:
return await self._async_validate_and_create()
return self.async_show_form(step_id="connection_error")
async def _async_validate_and_create(self) -> ConfigFlowResult:
"""Validate the OAuth token and create the config entry."""
assert self._oauth_data is not None
access_token = self._oauth_data[CONF_TOKEN][CONF_ACCESS_TOKEN]
self._access_token = user_input[CONF_LEGACY_ACCESS_TOKEN].replace(" ", "")
tibber_connection = tibber.Tibber(
access_token=access_token,
access_token=self._access_token,
websession=async_get_clientsession(self.hass),
)
self._title = tibber_connection.name or "Tibber"
errors: dict[str, str] = {}
try:
await tibber_connection.update_info()
except TimeoutError:
return await self.async_step_connection_error()
errors[CONF_LEGACY_ACCESS_TOKEN] = ERR_TIMEOUT
except tibber.InvalidLoginError:
return self.async_abort(reason=ERR_TOKEN)
errors[CONF_LEGACY_ACCESS_TOKEN] = ERR_TOKEN
except (
aiohttp.ClientError,
tibber.RetryableHttpExceptionError,
tibber.FatalHttpExceptionError,
):
return await self.async_step_connection_error()
except tibber.FatalHttpExceptionError:
return self.async_abort(reason=ERR_CLIENT)
errors[CONF_LEGACY_ACCESS_TOKEN] = ERR_CLIENT
if errors:
data_schema = self.add_suggested_values_to_schema(
DATA_SCHEMA, {CONF_LEGACY_ACCESS_TOKEN: self._access_token or ""}
)
return self.async_show_form(
step_id=SOURCE_USER,
data_schema=data_schema,
description_placeholders={"url": TOKEN_URL},
errors=errors,
)
await self.async_set_unique_id(tibber_connection.user_id)
title = tibber_connection.name or "Tibber"
if self.source == SOURCE_REAUTH:
reauth_entry = self._get_reauth_entry()
self._abort_if_unique_id_mismatch(
reason="wrong_account",
description_placeholders={"title": reauth_entry.title},
)
else:
self._abort_if_unique_id_configured()
return await self.async_step_pick_implementation()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle a reauth flow."""
reauth_entry = self._get_reauth_entry()
self._access_token = reauth_entry.data.get(CONF_LEGACY_ACCESS_TOKEN)
self._title = reauth_entry.title
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication by reusing the user step."""
reauth_entry = self._get_reauth_entry()
self._access_token = reauth_entry.data.get(CONF_LEGACY_ACCESS_TOKEN)
self._title = reauth_entry.title
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
)
return await self.async_step_user()
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Finalize the OAuth flow and create the config entry."""
if self._access_token is None:
return self.async_abort(reason="missing_configuration")
data[CONF_LEGACY_ACCESS_TOKEN] = self._access_token
access_token = data[CONF_TOKEN][CONF_ACCESS_TOKEN]
data_api_client = tibber_data_api.TibberDataAPI(
access_token,
websession=async_get_clientsession(self.hass),
)
try:
await data_api_client.get_userinfo()
except (aiohttp.ClientError, TimeoutError):
return self.async_abort(reason="cannot_connect")
if self.source == SOURCE_REAUTH:
reauth_entry = self._get_reauth_entry()
return self.async_update_reload_and_abort(
reauth_entry,
data=self._oauth_data,
title=title,
data=data,
title=self._title,
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=title, data=self._oauth_data)
return self.async_create_entry(title=self._title, data=data)

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN
if TYPE_CHECKING:
from . import TibberRuntimeData
@@ -12,6 +13,8 @@ if TYPE_CHECKING:
type TibberConfigEntry = ConfigEntry[TibberRuntimeData]
CONF_LEGACY_ACCESS_TOKEN = CONF_ACCESS_TOKEN
AUTH_IMPLEMENTATION = "auth_implementation"
DATA_HASS_CONFIG = "tibber_hass_config"
DOMAIN = "tibber"

View File

@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, cast
from aiohttp.client_exceptions import ClientError
import tibber
from tibber.data_api import TibberDevice
from tibber.data_api import TibberDataAPI, TibberDevice
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
@@ -230,26 +230,28 @@ class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
return device_sensors.get(sensor_id)
return None
async def _async_get_client(self) -> tibber.Tibber:
"""Get the Tibber client with error handling."""
async def _async_get_client(self) -> TibberDataAPI:
"""Get the Tibber Data API client with error handling."""
try:
return await self._runtime_data.async_get_client(self.hass)
except ConfigEntryAuthFailed:
raise
except (ClientError, TimeoutError, tibber.UserAgentMissingError) as err:
raise UpdateFailed(f"Unable to create Tibber client: {err}") from err
raise UpdateFailed(
f"Unable to create Tibber Data API client: {err}"
) from err
async def _async_setup(self) -> None:
"""Initial load of Tibber Data API devices."""
client = await self._async_get_client()
devices = await client.data_api.get_all_devices()
devices = await client.get_all_devices()
self._build_sensor_lookup(devices)
async def _async_update_data(self) -> dict[str, TibberDevice]:
"""Fetch the latest device capabilities from the Tibber Data API."""
client = await self._async_get_client()
try:
devices: dict[str, TibberDevice] = await client.data_api.update_devices()
devices: dict[str, TibberDevice] = await client.update_devices()
except tibber.exceptions.RateLimitExceededError as err:
raise UpdateFailed(
f"Rate limit exceeded, retry after {err.retry_after} seconds",

View File

@@ -15,7 +15,6 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""
runtime = config_entry.runtime_data
tibber_connection = await runtime.async_get_client(hass)
result: dict[str, Any] = {
"homes": [
{
@@ -25,7 +24,7 @@ async def async_get_config_entry_diagnostics(
"last_cons_data_timestamp": home.last_cons_data_timestamp,
"country": home.country,
}
for home in tibber_connection.get_homes(only_active=False)
for home in runtime.tibber_connection.get_homes(only_active=False)
]
}

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
import tibber
from homeassistant.components.notify import (
ATTR_TITLE_DEFAULT,
NotifyEntity,
@@ -39,9 +37,7 @@ class TibberNotificationEntity(NotifyEntity):
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to Tibber devices."""
tibber_connection: tibber.Tibber = (
await self._entry.runtime_data.async_get_client(self.hass)
)
tibber_connection = self._entry.runtime_data.tibber_connection
try:
await tibber_connection.send_notification(
title or ATTR_TITLE_DEFAULT, message

View File

@@ -605,7 +605,7 @@ async def _async_setup_graphql_sensors(
) -> None:
"""Set up the Tibber sensor."""
tibber_connection = await entry.runtime_data.async_get_client(hass)
tibber_connection = entry.runtime_data.tibber_connection
entity_registry = er.async_get(hass)

View File

@@ -42,7 +42,7 @@ async def __get_prices(call: ServiceCall) -> ServiceResponse:
translation_domain=DOMAIN,
translation_key="no_config_entry",
)
tibber_connection = await entries[0].runtime_data.async_get_client(call.hass)
tibber_connection = entries[0].runtime_data.tibber_connection
start = __get_date(call.data.get(ATTR_START), "start")
end = __get_date(call.data.get(ATTR_END), "end")

View File

@@ -2,21 +2,26 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"wrong_account": "The connected account does not match {title}. Sign in with the same Tibber account and try again."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"timeout": "[%key:common::config_flow::error::timeout_connect%]"
},
"step": {
"connection_error": {
"description": "Could not connect to Tibber. Check your internet connection and try again.",
"title": "Connection failed"
},
"reauth_confirm": {
"description": "Reconnect your Tibber account to refresh access.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"user": {
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]"
},
"description": "Enter your access token from {url}"
}
}
},

View File

@@ -83,14 +83,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return False
if entry.version == 2:
# 2 -> 2.2: Unique ID from integer to string
if entry.minor_version == 1:
minor_version = 2
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.unique_id), minor_version=minor_version
)
return True

View File

@@ -20,7 +20,6 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
DOMAIN = DOMAIN
VERSION = 2
MINOR_VERSION = 2
agreements: list[Agreement]
data: dict[str, Any]
@@ -93,7 +92,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
if self.migrate_entry:
await self.hass.config_entries.async_remove(self.migrate_entry)
await self.async_set_unique_id(str(agreement.agreement_id))
await self.async_set_unique_id(agreement.agreement_id)
self._abort_if_unique_id_configured()
self.data[CONF_AGREEMENT_ID] = agreement.agreement_id

View File

@@ -10,9 +10,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]",
"wrong_account": "Wrong account: Please authenticate with {username}."
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"reauth_confirm": {
"description": "The Twitch integration needs to re-authenticate your account",

View File

@@ -8,7 +8,6 @@ import dataclasses
from uiprotect.data import (
NVR,
Camera,
Event,
ModelType,
MountType,
ProtectAdoptableDeviceModel,
@@ -645,31 +644,6 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
self._attr_is_on = False
self._attr_extra_state_attributes = {}
@callback
def _find_active_event_with_object_type(
self, device: ProtectDeviceType
) -> Event | None:
"""Find an active event containing this sensor's object type.
Fallback for issue #152133: last_smart_detect_event_ids may not update
immediately when a new detection type is added to an ongoing event.
"""
obj_type = self.entity_description.ufp_obj_type
if obj_type is None or not isinstance(device, Camera):
return None
# Check known active event IDs from camera first (fast path)
for event_id in device.last_smart_detect_event_ids.values():
if (
event_id
and (event := self.data.api.bootstrap.events.get(event_id))
and event.end is None
and obj_type in event.smart_detect_types
):
return event
return None
@callback
def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
description = self.entity_description
@@ -677,15 +651,9 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
prev_event = self._event
prev_event_end = self._event_end
super()._async_update_device_from_protect(device)
event = description.get_event_obj(device)
if event is None:
# Fallback for #152133: check active events directly
event = self._find_active_event_with_object_type(device)
if event:
if event := description.get_event_obj(device):
self._event = event
self._event_end = event.end
self._event_end = event.end if event else None
if not (
event

View File

@@ -41,7 +41,7 @@
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["uiprotect==10.0.1", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==8.1.1", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -21,9 +21,6 @@
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"oauth_discovery": {
"description": "Home Assistant has found a Withings device on your network. Be aware that the setup of Withings is more complicated than many other integrations. Press **Submit** to continue setting up Withings."

View File

@@ -15,9 +15,6 @@
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"oauth_discovery": {
"description": "Home Assistant has found an Xbox device on your network. Press **Submit** to continue setting up the Xbox integration.",

View File

@@ -17,9 +17,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"channels": {
"data": { "channels": "YouTube channels" },

View File

@@ -294,6 +294,7 @@ FLOWS = {
"homekit",
"homekit_controller",
"homematicip_cloud",
"homevolt",
"homewizard",
"homeworks",
"honeywell",

View File

@@ -2836,6 +2836,12 @@
"zwave"
]
},
"homevolt": {
"name": "Homevolt",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"homewizard": {
"name": "HomeWizard",
"integration_type": "device",
@@ -5375,7 +5381,7 @@
"name": "QNAP"
},
"qnap_qsw": {
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling",
"name": "QNAP QSW"
@@ -5413,7 +5419,7 @@
},
"rabbitair": {
"name": "Rabbit Air",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -5438,7 +5444,7 @@
},
"radiotherm": {
"name": "Radio Thermostat",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -5473,7 +5479,7 @@
},
"rapt_ble": {
"name": "RAPT Bluetooth",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -5571,7 +5577,7 @@
},
"renson": {
"name": "Renson",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -5679,13 +5685,13 @@
},
"romy": {
"name": "ROMY Vacuum Cleaner",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"roomba": {
"name": "iRobot Roomba and Braava",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -5720,7 +5726,7 @@
},
"rova": {
"name": "ROVA",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -5763,13 +5769,13 @@
"name": "Ruuvi",
"integrations": {
"ruuvi_gateway": {
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling",
"name": "Ruuvi Gateway"
},
"ruuvitag_ble": {
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push",
"name": "Ruuvi BLE"
@@ -5784,7 +5790,7 @@
},
"sabnzbd": {
"name": "SABnzbd",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -5824,7 +5830,7 @@
},
"sanix": {
"name": "Sanix",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},

Some files were not shown because too many files have changed in this diff Show More