Compare commits

..

28 Commits

Author SHA1 Message Date
Daniel Hjelseth Høyer
dfa21fed00 Merge branch 'dev' into simplify_tibber_config 2026-01-18 07:07:50 +01:00
Daniel Hjelseth Høyer
0607627f8c Merge: resolve conflict in coordinator.py 2026-01-13 19:24:06 +01:00
Daniel Hjelseth Høyer
76f9e09527 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:48 +01:00
Daniel Hjelseth Høyer
a636705588 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:48 +01:00
Daniel Hjelseth Høyer
f5d0f347fb Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:48 +01:00
Daniel Hjelseth Høyer
89418f579f Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:48 +01:00
Daniel Hjelseth Høyer
e390848477 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:48 +01:00
Daniel Hjelseth Høyer
6fc7a43193 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:47 +01:00
Daniel Hjelseth Høyer
5e7c3c6a47 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:47 +01:00
Daniel Hjelseth Høyer
003f53f109 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:47 +01:00
Daniel Hjelseth Høyer
2533e37464 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:47 +01:00
Daniel Hjelseth Høyer
fed4bdb09b Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:46 +01:00
Daniel Hjelseth Høyer
17e8c09e2b Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:46 +01:00
Daniel Hjelseth Høyer
7006efca92 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:46 +01:00
Daniel Hjelseth Høyer
96130c093b Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-13 19:18:45 +01:00
Daniel Hjelseth Høyer
04b8e3d62e Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 19:56:17 +01:00
Daniel Hjelseth Høyer
14b31ba458 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 17:23:26 +01:00
Daniel Hjelseth Høyer
3d70727e34 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 16:59:10 +01:00
Daniel Hjelseth Høyer
a84eb61484 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 10:59:47 +01:00
Daniel Hjelseth Høyer
1cb6510b49 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 10:39:33 +01:00
Daniel Hjelseth Høyer
e6a72b9c85 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 09:08:13 +01:00
Daniel Hjelseth Høyer
dcb12b0779 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 08:54:16 +01:00
Daniel Hjelseth Høyer
dc6dda20d8 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 08:47:36 +01:00
Daniel Hjelseth Høyer
f59a777855 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 21:14:28 +01:00
Daniel Hjelseth Høyer
828f409ab9 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 21:07:16 +01:00
Daniel Hjelseth Høyer
53cf731578 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 20:22:14 +01:00
Daniel Hjelseth Høyer
aa0c5dcc01 Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 08:21:17 +01:00
Daniel Hjelseth Høyer
3914d196db Simplify Tibber setup
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 07:42:38 +01:00
294 changed files with 1721 additions and 11288 deletions

View File

@@ -310,7 +310,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: &key-python-venv >-
@@ -374,7 +374,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: *path-apt-cache
key: *key-apt-cache
@@ -425,7 +425,7 @@ jobs:
steps:
- &cache-restore-apt
name: Restore apt cache
uses: &actions-cache-restore actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: *path-apt-cache
fail-on-cache-miss: true

View File

@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt: |

View File

@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o-mini
system-prompt: |

View File

@@ -52,7 +52,7 @@ class AdGuardHomeEntity(Entity):
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
if self._entry.source == SOURCE_HASSIO:
config_url = "homeassistant://app/a0d7b954_adguard"
config_url = "homeassistant://hassio/ingress/a0d7b954_adguard"
elif self.adguard.tls:
config_url = f"https://{self.adguard.host}:{self.adguard.port}"
else:

View File

@@ -127,7 +127,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"assist_satellite",
"fan",
"light",
"siren",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
@@ -602,10 +601,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced labels."""
referenced = self.action_script.referenced_labels
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_labels(conf)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
return referenced
@@ -615,10 +610,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced floors."""
referenced = self.action_script.referenced_floors
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_floors(conf)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
return referenced
@@ -628,10 +619,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced areas."""
referenced = self.action_script.referenced_areas
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_areas(conf)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
return referenced

View File

@@ -85,9 +85,9 @@
}
},
"moving": {
"default": "mdi:octagon",
"default": "mdi:arrow-right",
"state": {
"on": "mdi:arrow-right"
"on": "mdi:octagon"
}
},
"occupancy": {

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.11.0"],
"requirements": ["hass-nabucasa==1.9.0"],
"single_config_entry": true
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.4.2"]
"requirements": ["compit-inext-api==0.3.4"]
}

View File

@@ -10,7 +10,7 @@ LOGGER = logging.getLogger(__package__)
DOMAIN = "deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://app/core_deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://hassio/ingress/core_deconz"
CONF_BRIDGE_ID = "bridgeid"
CONF_GROUP_ID_BASE = "group_id_base"

View File

@@ -1034,7 +1034,7 @@ def _async_setup_device_registry(
and dashboard.data
and dashboard.data.get(device_info.name)
):
configuration_url = f"homeassistant://app/{dashboard.addon_slug}"
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
manufacturer = "espressif"
if device_info.manufacturer:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyfirefly==0.1.12"]
"requirements": ["pyfirefly==0.1.11"]
}

View File

@@ -16,7 +16,7 @@ from aiohasupervisor.models import GreenOptions, YellowOptions # noqa: F401
import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import frontend, panel_custom
from homeassistant.components import panel_custom
from homeassistant.components.homeassistant import async_set_stop_handler
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
@@ -292,7 +292,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
return False
async_load_websocket_api(hass)
frontend.async_register_built_in_panel(hass, "app")
host = os.environ["SUPERVISOR"]
websession = async_get_clientsession(hass)

View File

@@ -6,7 +6,10 @@ from dataclasses import dataclass
from pyHomee.const import AttributeType, NodeState
from pyHomee.model import HomeeAttribute, HomeeNode
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -14,10 +17,17 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from . import HomeeConfigEntry
from .const import (
DOMAIN,
HOMEE_UNIT_TO_HA_UNIT,
OPEN_CLOSE_MAP,
OPEN_CLOSE_MAP_REVERSED,
@@ -99,6 +109,11 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.CURRENT_VALVE_POSITION: HomeeSensorEntityDescription(
key="valve_position",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.DAWN: HomeeSensorEntityDescription(
key="dawn",
device_class=SensorDeviceClass.ILLUMINANCE,
@@ -279,12 +294,57 @@ NODE_SENSOR_DESCRIPTIONS: tuple[HomeeNodeSensorEntityDescription, ...] = (
)
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Get list of related automations and scripts."""
used_in = automations_with_entity(hass, entity_id)
used_in += scripts_with_entity(hass, entity_id)
return used_in
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the homee platform for the sensor components."""
ent_reg = er.async_get(hass)
def add_deprecated_entity(
attribute: HomeeAttribute, description: HomeeSensorEntityDescription
) -> list[HomeeSensor]:
"""Add deprecated entities."""
deprecated_entities: list[HomeeSensor] = []
entity_uid = f"{config_entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, entity_uid):
entity_entry = ent_reg.async_get(entity_id)
if entity_entry and entity_entry.disabled:
ent_reg.async_remove(entity_id)
async_delete_issue(
hass,
DOMAIN,
f"deprecated_entity_{entity_uid}",
)
elif entity_entry:
deprecated_entities.append(
HomeeSensor(attribute, config_entry, description)
)
if entity_used_in(hass, entity_id):
async_create_issue(
hass,
DOMAIN,
f"deprecated_entity_{entity_uid}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders={
"name": str(
entity_entry.name or entity_entry.original_name
),
"entity": entity_id,
},
)
return deprecated_entities
async def add_sensor_entities(
config_entry: HomeeConfigEntry,
@@ -302,13 +362,19 @@ async def async_setup_entry(
)
# Node attributes that are sensors.
entities.extend(
HomeeSensor(
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
)
for attribute in node.attributes
if attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable
)
for attribute in node.attributes:
if attribute.type == AttributeType.CURRENT_VALVE_POSITION:
entities.extend(
add_deprecated_entity(
attribute, SENSOR_DESCRIPTIONS[attribute.type]
)
)
elif attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable:
entities.append(
HomeeSensor(
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
)
)
if entities:
async_add_entities(entities)

View File

@@ -495,5 +495,11 @@
"invalid_preset_mode": {
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
}
},
"issues": {
"deprecated_entity": {
"description": "The Homee entity `{entity}` is deprecated and will be removed in release 2025.12.\nThe valve is available directly in the respective climate entity.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue.",
"title": "The Homee {name} entity is deprecated"
}
}
}

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": "hub",
"iot_class": "cloud_polling",
"loggers": ["keyrings.alt", "pyicloud"],
"requirements": ["pyicloud==2.3.0"]
"requirements": ["pyicloud==2.2.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

@@ -41,7 +41,7 @@
"title": "Lawn mower",
"triggers": {
"docked": {
"description": "Triggers after one or more lawn mowers have returned to dock.",
"description": "Triggers after one or more lawn mowers return to dock.",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",

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

@@ -50,7 +50,7 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Check connection to the Mealie API."""
assert self.host is not None
if "/app/" in self.host:
if "/hassio/ingress/" in self.host:
return {"base": "ingress_url"}, None
client = MealieClient(

View File

@@ -73,6 +73,15 @@ SHARED_OPTIONS = [
CONF_STATE_TOPIC,
]
MQTT_ORIGIN_INFO_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_SW_VERSION): cv.string,
vol.Optional(CONF_SUPPORT_URL): cv.configuration_url,
}
),
)
_MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
{

View File

@@ -125,7 +125,7 @@ class NumberDeviceClass(StrEnum):
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
Unit of measurement: `ppm` (parts per million), `mg/m³`, `μg/m³`
"""
CO2 = "carbon_dioxide"
@@ -373,7 +373,7 @@ class NumberDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `ppb` (parts per billion), `μg/m³`
Unit of measurement: `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -483,7 +483,6 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.BATTERY: {PERCENTAGE},
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
NumberDeviceClass.CO: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -546,10 +545,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
},
NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
NumberDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
NumberDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature),
NumberDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {

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

@@ -158,7 +158,7 @@ MODEL_NAMES = [ # https://ollama.com/library
"yi",
"zephyr",
]
DEFAULT_MODEL = "qwen3:4b-instruct"
DEFAULT_MODEL = "qwen3:4b"
DEFAULT_CONVERSATION_NAME = "Ollama Conversation"
DEFAULT_AI_TASK_NAME = "Ollama AI Task"

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

@@ -10,6 +10,7 @@ from openevsehttp.__main__ import OpenEVSE
import voluptuous as vol
from homeassistant.components.sensor import (
DOMAIN as HOMEASSISTANT_DOMAIN,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
@@ -26,7 +27,7 @@ from homeassistant.const import (
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo

View File

@@ -55,10 +55,6 @@
}
},
"issues": {
"deprecated_yaml_import_issue_unavailable_host": {
"description": "Configuring {integration_title} using YAML is being removed but there was a connection error while trying to import the YAML configuration.\n\nEnsure your OpenEVSE charger is accessible and restart Home Assistant to try again.",
"title": "The {integration_title} YAML configuration import failed"
},
"yaml_deprecated": {
"description": "Configuring OpenEVSE using YAML is being removed. Your existing YAML configuration has been imported into the UI automatically. Remove the `openevse` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"title": "OpenEVSE YAML configuration is deprecated"

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.16.4"]
"requirements": ["opower==0.16.3"]
}

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

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
from pyqwikswitch.qwikswitch import SENSORS
@@ -40,7 +39,9 @@ async def async_setup_platform(
class QSBinarySensor(QSEntity, BinarySensorEntity):
"""Sensor based on a Qwikswitch relay/dimmer module."""
def __init__(self, sensor: dict[str, Any]) -> None:
_val = False
def __init__(self, sensor):
"""Initialize the sensor."""
super().__init__(sensor["id"], sensor["name"])
@@ -49,9 +50,7 @@ class QSBinarySensor(QSEntity, BinarySensorEntity):
self._decode, _ = SENSORS[sensor_type]
self._invert = not sensor.get("invert", False)
self._attr_is_on = not self._invert
self._attr_device_class = sensor.get("class", BinarySensorDeviceClass.DOOR)
self._attr_unique_id = f"qs{self.qsid}:{self.channel}"
self._class = sensor.get("class", "door")
@callback
def update_packet(self, packet):
@@ -66,5 +65,20 @@ class QSBinarySensor(QSEntity, BinarySensorEntity):
packet,
)
if val is not None:
self._attr_is_on = bool(val) == self._invert
self._val = bool(val)
self.async_write_ha_state()
@property
def is_on(self):
"""Check if device is on (non-zero)."""
return self._val == self._invert
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return f"qs{self.qsid}:{self.channel}"
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor."""
return self._class

View File

@@ -15,12 +15,21 @@ class QSEntity(Entity):
_attr_should_poll = False
def __init__(self, qsid: str, name: str) -> None:
def __init__(self, qsid, name):
"""Initialize the QSEntity."""
self._attr_name = name
self._attr_unique_id = f"qs{qsid}"
self._name = name
self.qsid = qsid
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return f"qs{self.qsid}"
@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB. Match dispather_send signature."""

View File

@@ -37,24 +37,21 @@ async def async_setup_platform(
class QSSensor(QSEntity, SensorEntity):
"""Sensor based on a Qwikswitch relay/dimmer module."""
def __init__(self, sensor: dict[str, Any]) -> None:
_val: Any | None = None
def __init__(self, sensor):
"""Initialize the sensor."""
super().__init__(sensor["id"], sensor["name"])
self.channel = sensor["channel"]
sensor_type = sensor["type"]
self._attr_unique_id = f"qs{self.qsid}:{self.channel}"
decode, unit = SENSORS[sensor_type]
self._decode, self.unit = SENSORS[sensor_type]
# this cannot happen because it only happens in bool and this should be redirected to binary_sensor
assert not isinstance(unit, type), (
assert not isinstance(self.unit, type), (
f"boolean sensor id={sensor['id']} name={sensor['name']}"
)
self._decode = decode
self._attr_native_unit_of_measurement = unit
@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
@@ -68,5 +65,20 @@ class QSSensor(QSEntity, SensorEntity):
packet,
)
if val is not None:
self._attr_native_value = str(val)
self._val = val
self.async_write_ha_state()
@property
def native_value(self):
"""Return the value of the sensor."""
return None if self._val is None else str(self._val)
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return f"qs{self.qsid}:{self.channel}"
@property
def native_unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self.unit

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

@@ -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

@@ -64,7 +64,6 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -226,7 +225,6 @@ _PRIMARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
_SECONDARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
CarbonMonoxideConcentrationConverter,
TemperatureDeltaConverter,
SulphurDioxideConcentrationConverter,
]
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {

View File

@@ -38,7 +38,6 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -95,9 +94,6 @@ UNIT_SCHEMA = vol.Schema(
vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS),
vol.Optional("reactive_power"): vol.In(ReactivePowerConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional("sulphur_dioxide"): vol.In(
SulphurDioxideConcentrationConverter.VALID_UNITS
),
vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional("temperature_delta"): vol.In(
TemperatureDeltaConverter.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

@@ -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

@@ -44,6 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.client.async_close()
coordinator = entry.runtime_data
coordinator.client.close()
return unload_ok

View File

@@ -22,17 +22,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import LeilSaunaConfigEntry, LeilSaunaCoordinator
from .const import (
DEFAULT_PRESET_NAME_TYPE_1,
DEFAULT_PRESET_NAME_TYPE_2,
DEFAULT_PRESET_NAME_TYPE_3,
DELAYED_REFRESH_SECONDS,
DOMAIN,
OPT_PRESET_NAME_TYPE_1,
OPT_PRESET_NAME_TYPE_2,
OPT_PRESET_NAME_TYPE_3,
)
from . import LeilSaunaConfigEntry
from .const import DELAYED_REFRESH_SECONDS, DOMAIN
from .entity import LeilSaunaEntity
PARALLEL_UPDATES = 1
@@ -61,12 +52,9 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
"""Representation of a Saunum Leil Sauna climate entity."""
_attr_name = None
_attr_translation_key = "saunum_climate"
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_precision = PRECISION_WHOLE
@@ -74,38 +62,6 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
_attr_min_temp = MIN_TEMPERATURE
_attr_max_temp = MAX_TEMPERATURE
_attr_fan_modes = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
_preset_name_map: dict[int, str]
def __init__(self, coordinator: LeilSaunaCoordinator) -> None:
"""Initialize the climate entity."""
super().__init__(coordinator)
self._update_preset_names()
def _update_preset_names(self) -> None:
"""Update preset names from config entry options."""
options = self.coordinator.config_entry.options
self._preset_name_map = {
0: options.get(OPT_PRESET_NAME_TYPE_1, DEFAULT_PRESET_NAME_TYPE_1),
1: options.get(OPT_PRESET_NAME_TYPE_2, DEFAULT_PRESET_NAME_TYPE_2),
2: options.get(OPT_PRESET_NAME_TYPE_3, DEFAULT_PRESET_NAME_TYPE_3),
}
self._attr_preset_modes = list(self._preset_name_map.values())
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.config_entry.add_update_listener(
self._async_update_listener
)
)
async def _async_update_listener(
self, hass: HomeAssistant, entry: LeilSaunaConfigEntry
) -> None:
"""Handle options update."""
self._update_preset_names()
self.async_write_ha_state()
@property
def current_temperature(self) -> float | None:
@@ -144,14 +100,6 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
else HVACAction.IDLE
)
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
sauna_type = self.coordinator.data.sauna_type
if sauna_type is not None and sauna_type in self._preset_name_map:
return self._preset_name_map[sauna_type]
return self._preset_name_map[0]
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new HVAC mode."""
if hvac_mode == HVACMode.HEAT and self.coordinator.data.door_open:
@@ -212,29 +160,3 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
) from err
await self.coordinator.async_request_refresh()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode (sauna type)."""
if self.coordinator.data.session_active:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="preset_session_active",
)
# Find the sauna type value from the preset name
sauna_type_value = 0 # Default to type 1
for type_value, type_name in self._preset_name_map.items():
if type_name == preset_mode:
sauna_type_value = type_value
break
try:
await self.coordinator.client.async_set_sauna_type(sauna_type_value)
except SaunumException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_preset_failed",
translation_placeholders={"preset_mode": preset_mode},
) from err
await self.coordinator.async_request_refresh()

View File

@@ -8,26 +8,11 @@ from typing import Any
from pysaunum import SaunumClient, SaunumException
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_USER,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from . import LeilSaunaConfigEntry
from .const import (
DEFAULT_PRESET_NAME_TYPE_1,
DEFAULT_PRESET_NAME_TYPE_2,
DEFAULT_PRESET_NAME_TYPE_3,
DOMAIN,
OPT_PRESET_NAME_TYPE_1,
OPT_PRESET_NAME_TYPE_2,
OPT_PRESET_NAME_TYPE_3,
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -51,7 +36,7 @@ async def validate_input(data: dict[str, Any]) -> None:
# Try to read data to verify communication
await client.async_get_data()
finally:
await client.async_close()
client.close()
class LeilSaunaConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -60,14 +45,6 @@ class LeilSaunaConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: LeilSaunaConfigEntry,
) -> LeilSaunaOptionsFlow:
"""Get the options flow for this handler."""
return LeilSaunaOptionsFlow()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -105,40 +82,3 @@ class LeilSaunaConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
class LeilSaunaOptionsFlow(OptionsFlow):
"""Handle options flow for Saunum Leil Sauna Control Unit."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options for preset mode names."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
OPT_PRESET_NAME_TYPE_1,
default=self.config_entry.options.get(
OPT_PRESET_NAME_TYPE_1, DEFAULT_PRESET_NAME_TYPE_1
),
): cv.string,
vol.Optional(
OPT_PRESET_NAME_TYPE_2,
default=self.config_entry.options.get(
OPT_PRESET_NAME_TYPE_2, DEFAULT_PRESET_NAME_TYPE_2
),
): cv.string,
vol.Optional(
OPT_PRESET_NAME_TYPE_3,
default=self.config_entry.options.get(
OPT_PRESET_NAME_TYPE_3, DEFAULT_PRESET_NAME_TYPE_3
),
): cv.string,
}
),
)

View File

@@ -7,13 +7,3 @@ DOMAIN: Final = "saunum"
DEFAULT_SCAN_INTERVAL: Final = timedelta(seconds=60)
DELAYED_REFRESH_SECONDS: Final = timedelta(seconds=3)
# Option keys for preset names
OPT_PRESET_NAME_TYPE_1: Final = "preset_name_type_1"
OPT_PRESET_NAME_TYPE_2: Final = "preset_name_type_2"
OPT_PRESET_NAME_TYPE_3: Final = "preset_name_type_3"
# Default preset names (translation keys)
DEFAULT_PRESET_NAME_TYPE_1: Final = "type_1"
DEFAULT_PRESET_NAME_TYPE_2: Final = "type_2"
DEFAULT_PRESET_NAME_TYPE_3: Final = "type_3"

View File

@@ -23,7 +23,6 @@ async def async_get_config_entry_diagnostics(
# Build diagnostics data
diagnostics_data: dict[str, Any] = {
"config": async_redact_data(entry.data, REDACT_CONFIG),
"options": dict(entry.options),
"client_info": {"connected": coordinator.client.is_connected},
"coordinator_info": {
"last_update_success": coordinator.last_update_success,

View File

@@ -1,19 +1,5 @@
{
"entity": {
"climate": {
"saunum_climate": {
"state_attributes": {
"preset_mode": {
"default": "mdi:heat-wave",
"state": {
"type_1": "mdi:numeric-1-box-outline",
"type_2": "mdi:numeric-2-box-outline",
"type_3": "mdi:numeric-3-box-outline"
}
}
}
}
},
"number": {
"fan_duration": {
"default": "mdi:fan-clock"

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["pysaunum"],
"quality_scale": "platinum",
"requirements": ["pysaunum==0.3.0"]
"requirements": ["pysaunum==0.2.0"]
}

View File

@@ -50,19 +50,6 @@
"name": "Thermal cutoff alarm"
}
},
"climate": {
"saunum_climate": {
"state_attributes": {
"preset_mode": {
"state": {
"type_1": "Sauna Type 1",
"type_2": "Sauna Type 2",
"type_3": "Sauna Type 3"
}
}
}
}
},
"light": {
"light": {
"name": "[%key:component::light::title%]"
@@ -93,14 +80,11 @@
"door_open": {
"message": "Cannot start sauna session when sauna door is open"
},
"preset_session_active": {
"message": "Cannot change preset mode while sauna session is active"
},
"session_active_cannot_change_fan_duration": {
"message": "Cannot change fan duration while sauna session is active"
"message": "Cannot change fan duration while session is active"
},
"session_active_cannot_change_sauna_duration": {
"message": "Cannot change sauna duration while sauna session is active"
"message": "Cannot change sauna duration while session is active"
},
"session_not_active": {
"message": "Cannot change fan mode when sauna session is not active"
@@ -120,31 +104,11 @@
"set_light_on_failed": {
"message": "Failed to turn on light"
},
"set_preset_failed": {
"message": "Failed to set preset to {preset_mode}"
},
"set_sauna_duration_failed": {
"message": "Failed to set sauna duration"
},
"set_temperature_failed": {
"message": "Failed to set temperature to {temperature}"
}
},
"options": {
"step": {
"init": {
"data": {
"preset_name_type_1": "Preset name for sauna type 1",
"preset_name_type_2": "Preset name for sauna type 2",
"preset_name_type_3": "Preset name for sauna type 3"
},
"data_description": {
"preset_name_type_1": "Custom name for sauna type 1 preset mode",
"preset_name_type_2": "Custom name for sauna type 2 preset mode",
"preset_name_type_3": "Custom name for sauna type 3 preset mode"
},
"description": "Customize the names of the three sauna type preset modes"
}
}
}
}

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

@@ -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

@@ -68,7 +68,6 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -159,7 +158,7 @@ class SensorDeviceClass(StrEnum):
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
Unit of measurement: `ppm` (parts per million), `mg/m³`, `μg/m³`
"""
CO2 = "carbon_dioxide"
@@ -410,7 +409,7 @@ class SensorDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `ppb` (parts per billion), `μg/m³`
Unit of measurement: `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -570,7 +569,6 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.PRESSURE: PressureConverter,
SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter,
SensorDeviceClass.REACTIVE_POWER: ReactivePowerConverter,
SensorDeviceClass.SULPHUR_DIOXIDE: SulphurDioxideConcentrationConverter,
SensorDeviceClass.SPEED: SpeedConverter,
SensorDeviceClass.TEMPERATURE: TemperatureConverter,
SensorDeviceClass.TEMPERATURE_DELTA: TemperatureDeltaConverter,
@@ -597,7 +595,6 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.BATTERY: {PERCENTAGE},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
SensorDeviceClass.CO: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -660,10 +657,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
},
SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
SensorDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
SensorDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature),
SensorDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {

View File

@@ -1,17 +0,0 @@
"""Provides conditions for sirens."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the siren conditions."""
return CONDITIONS

View File

@@ -1,17 +0,0 @@
.condition_common: &condition_common
target:
entity:
domain: siren
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_off: *condition_common
is_on: *condition_common

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_off": {
"condition": "mdi:bullhorn-outline"
},
"is_on": {
"condition": "mdi:bullhorn"
}
},
"entity_component": {
"_": {
"default": "mdi:bullhorn"

View File

@@ -1,32 +1,8 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted sirens.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted sirens to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_off": {
"description": "Tests if one or more sirens are off.",
"fields": {
"behavior": {
"description": "[%key:component::siren::common::condition_behavior_description%]",
"name": "[%key:component::siren::common::condition_behavior_name%]"
}
},
"name": "If a siren is off"
},
"is_on": {
"description": "Tests if one or more sirens are on.",
"fields": {
"behavior": {
"description": "[%key:component::siren::common::condition_behavior_description%]",
"name": "[%key:component::siren::common::condition_behavior_name%]"
}
},
"name": "If a siren is on"
}
},
"entity_component": {
"_": {
"name": "[%key:component::siren::title%]",
@@ -42,12 +18,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

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

@@ -16,9 +16,6 @@
"create_entry": {
"default": "Successfully authenticated with Spotify."
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"pick_implementation": {
"data": {

View File

@@ -185,9 +185,6 @@ async def make_device_data(
"Smart Lock Lite",
"Smart Lock Pro",
"Smart Lock Ultra",
"Smart Lock Vision",
"Smart Lock Vision Pro",
"Smart Lock Pro Wifi",
]:
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id

View File

@@ -92,18 +92,6 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Smart Lock Vision": (
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Smart Lock Vision Pro": (
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Smart Lock Pro Wifi": (
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Curtain": (CALIBRATION_DESCRIPTION,),
"Curtain3": (CALIBRATION_DESCRIPTION,),
"Roller Shade": (CALIBRATION_DESCRIPTION,),

View File

@@ -46,7 +46,7 @@ class SwitchBotCloudLock(SwitchBotCloudEntity, LockEntity):
"""Set attributes from coordinator data."""
if coord_data := self.coordinator.data:
self._attr_is_locked = coord_data["lockState"] == "locked"
if self.__model != "Smart Lock Lite":
if self.__model in LockV2Commands.get_supported_devices():
self._attr_supported_features = LockEntityFeature.OPEN
async def async_lock(self, **kwargs: Any) -> None:

View File

@@ -225,9 +225,6 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
"Smart Lock Lite": (BATTERY_DESCRIPTION,),
"Smart Lock Pro": (BATTERY_DESCRIPTION,),
"Smart Lock Ultra": (BATTERY_DESCRIPTION,),
"Smart Lock Vision": (BATTERY_DESCRIPTION,),
"Smart Lock Vision Pro": (BATTERY_DESCRIPTION,),
"Smart Lock Pro Wifi": (BATTERY_DESCRIPTION,),
"Relay Switch 2PM": (
RELAY_SWITCH_2PM_POWER_DESCRIPTION,
RELAY_SWITCH_2PM_VOLTAGE_DESCRIPTION,

View File

@@ -18,11 +18,7 @@ from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlowResult,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -77,11 +73,6 @@ class OAuth2FlowHandler(
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=data
)
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch(reason="reconfigure_account_mismatch")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(), data=data
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
@@ -130,9 +121,3 @@ class OAuth2FlowHandler(
)
return await super().async_step_user()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration."""
return await self.async_step_user()

View File

@@ -33,9 +33,7 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_account_mismatch": "The reauthentication account does not match the original account",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_account_mismatch": "The reconfiguration account does not match the original account",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",

View File

@@ -8,7 +8,6 @@ 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
@@ -23,13 +22,7 @@ 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,
CONF_LEGACY_ACCESS_TOKEN,
DATA_HASS_CONFIG,
DOMAIN,
TibberConfigEntry,
)
from .const import AUTH_IMPLEMENTATION, DATA_HASS_CONFIG, DOMAIN, TibberConfigEntry
from .coordinator import TibberDataAPICoordinator
from .services import async_setup_services
@@ -44,24 +37,23 @@ _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_data_api.TibberDataAPI | None = None
_client: tibber.Tibber | None = None
async def async_get_client(
self, hass: HomeAssistant
) -> tibber_data_api.TibberDataAPI:
"""Return an authenticated Tibber Data API client."""
async def async_get_client(self, hass: HomeAssistant) -> tibber.Tibber:
"""Return an authenticated Tibber 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_data_api.TibberDataAPI(
access_token,
self._client = tibber.Tibber(
access_token=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
@@ -88,32 +80,6 @@ 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:
@@ -135,10 +101,29 @@ 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
@@ -154,5 +139,6 @@ async def async_unload_entry(
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
await config_entry.runtime_data.tibber_connection.rt_disconnect()
tibber_connection = await config_entry.runtime_data.async_get_client(hass)
await tibber_connection.rt_disconnect()
return unload_ok

View File

@@ -14,7 +14,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -49,12 +48,6 @@ DATA_API_BINARY_SENSORS: tuple[TibberBinarySensorEntityDescription, ...] = (
device_class=BinarySensorDeviceClass.POWER,
is_on_fn={"on": True, "off": False}.get,
),
TibberBinarySensorEntityDescription(
key="isOnline",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda v: v.lower() == "true",
entity_category=EntityCategory.DIAGNOSTIC,
),
)

View File

@@ -8,21 +8,16 @@ 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, SOURCE_USER, ConfigFlowResult
from homeassistant.config_entries import SOURCE_REAUTH, 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 CONF_LEGACY_ACCESS_TOKEN, DATA_API_DEFAULT_SCOPES, DOMAIN
from .const import 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__)
@@ -36,8 +31,7 @@ class TibberConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
super().__init__()
self._access_token: str | None = None
self._title = ""
self._oauth_data: dict[str, Any] | None = None
@property
def logger(self) -> logging.Logger:
@@ -52,114 +46,70 @@ class TibberConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"scope": " ".join(DATA_API_DEFAULT_SCOPES),
}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
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={},
)
self._access_token = user_input[CONF_LEGACY_ACCESS_TOKEN].replace(" ", "")
tibber_connection = tibber.Tibber(
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:
errors[CONF_LEGACY_ACCESS_TOKEN] = ERR_TIMEOUT
except tibber.InvalidLoginError:
errors[CONF_LEGACY_ACCESS_TOKEN] = ERR_TOKEN
except (
aiohttp.ClientError,
tibber.RetryableHttpExceptionError,
tibber.FatalHttpExceptionError,
):
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)
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 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")
self._oauth_data = data
return await self._async_validate_and_create()
data[CONF_LEGACY_ACCESS_TOKEN] = self._access_token
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")
access_token = data[CONF_TOKEN][CONF_ACCESS_TOKEN]
data_api_client = tibber_data_api.TibberDataAPI(
access_token,
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]
tibber_connection = tibber.Tibber(
access_token=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")
await tibber_connection.update_info()
except TimeoutError:
return await self.async_step_connection_error()
except tibber.InvalidLoginError:
return self.async_abort(reason=ERR_TOKEN)
except (
aiohttp.ClientError,
tibber.RetryableHttpExceptionError,
):
return await self.async_step_connection_error()
except tibber.FatalHttpExceptionError:
return self.async_abort(reason=ERR_CLIENT)
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},
)
return self.async_update_reload_and_abort(
reauth_entry,
data=data,
title=self._title,
data=self._oauth_data,
title=title,
)
return self.async_create_entry(title=self._title, data=data)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=title, data=self._oauth_data)

View File

@@ -5,7 +5,6 @@ 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
@@ -13,8 +12,6 @@ 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 TibberDataAPI, TibberDevice
from tibber.data_api import TibberDevice
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
@@ -230,28 +230,26 @@ class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
return device_sensors.get(sensor_id)
return None
async def _async_get_client(self) -> TibberDataAPI:
"""Get the Tibber Data API client with error handling."""
async def _async_get_client(self) -> tibber.Tibber:
"""Get the Tibber 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 Data API client: {err}"
) from err
raise UpdateFailed(f"Unable to create Tibber 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.get_all_devices()
devices = await client.data_api.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.update_devices()
devices: dict[str, TibberDevice] = await client.data_api.update_devices()
except tibber.exceptions.RateLimitExceededError as err:
raise UpdateFailed(
f"Rate limit exceeded, retry after {err.retry_after} seconds",

View File

@@ -15,6 +15,7 @@ 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": [
{
@@ -24,7 +25,7 @@ async def async_get_config_entry_diagnostics(
"last_cons_data_timestamp": home.last_cons_data_timestamp,
"country": home.country,
}
for home in runtime.tibber_connection.get_homes(only_active=False)
for home in tibber_connection.get_homes(only_active=False)
]
}

View File

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

View File

@@ -264,15 +264,6 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
DATA_API_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="cellular.rssi",
translation_key="cellular_rssi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="storage.stateOfCharge",
translation_key="storage_state_of_charge",
@@ -287,254 +278,6 @@ DATA_API_SENSORS: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="storage.ratedCapacity",
translation_key="storage_rated_capacity",
device_class=SensorDeviceClass.ENERGY_STORAGE,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="storage.ratedPower",
translation_key="storage_rated_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="storage.availableEnergy",
translation_key="storage_available_energy",
device_class=SensorDeviceClass.ENERGY_STORAGE,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.battery.power",
translation_key="power_flow_battery",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.grid.power",
translation_key="power_flow_grid",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.load.power",
translation_key="power_flow_load",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.toGrid",
translation_key="power_flow_to_grid",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.toLoad",
translation_key="power_flow_to_load",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.fromGrid",
translation_key="power_flow_from_grid",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="powerFlow.fromLoad",
translation_key="power_flow_from_load",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="energyFlow.hour.battery.charged",
translation_key="energy_flow_hour_battery_charged",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.hour.battery.discharged",
translation_key="energy_flow_hour_battery_discharged",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.hour.battery.source.grid",
translation_key="energy_flow_hour_battery_source_grid",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.hour.battery.source.load",
translation_key="energy_flow_hour_battery_source_load",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.hour.grid.imported",
translation_key="energy_flow_hour_grid_imported",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.hour.grid.exported",
translation_key="energy_flow_hour_grid_exported",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.hour.load.consumed",
translation_key="energy_flow_hour_load_consumed",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.hour.load.generated",
translation_key="energy_flow_hour_load_generated",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.hour.load.source.battery",
translation_key="energy_flow_hour_load_source_battery",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.hour.load.source.grid",
translation_key="energy_flow_hour_load_source_grid",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.battery.charged",
translation_key="energy_flow_month_battery_charged",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.month.battery.discharged",
translation_key="energy_flow_month_battery_discharged",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.month.battery.source.grid",
translation_key="energy_flow_month_battery_source_grid",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.battery.source.battery",
translation_key="energy_flow_month_battery_source_battery",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.battery.source.load",
translation_key="energy_flow_month_battery_source_load",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.grid.imported",
translation_key="energy_flow_month_grid_imported",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.month.grid.exported",
translation_key="energy_flow_month_grid_exported",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.month.grid.source.battery",
translation_key="energy_flow_month_grid_source_battery",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.grid.source.grid",
translation_key="energy_flow_month_grid_source_grid",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.grid.source.load",
translation_key="energy_flow_month_grid_source_load",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.load.consumed",
translation_key="energy_flow_month_load_consumed",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.month.load.generated",
translation_key="energy_flow_month_load_generated",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="energyFlow.month.load.source.battery",
translation_key="energy_flow_month_load_source_battery",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energyFlow.month.load.source.grid",
translation_key="energy_flow_month_load_source_grid",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="range.remaining",
translation_key="range_remaining",
@@ -605,7 +348,7 @@ async def _async_setup_graphql_sensors(
) -> None:
"""Set up the Tibber sensor."""
tibber_connection = entry.runtime_data.tibber_connection
tibber_connection = await entry.runtime_data.async_get_client(hass)
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 = entries[0].runtime_data.tibber_connection
tibber_connection = await entries[0].runtime_data.async_get_client(call.hass)
start = __get_date(call.data.get(ATTR_START), "start")
end = __get_date(call.data.get(ATTR_END), "end")

View File

@@ -2,26 +2,21 @@
"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}"
}
}
},
@@ -48,9 +43,6 @@
"average_power": {
"name": "Average power"
},
"cellular_rssi": {
"name": "Cellular signal strength"
},
"charging_current_max": {
"name": "Maximum allowed charge current"
},
@@ -69,72 +61,6 @@
"electricity_price": {
"name": "Electricity price"
},
"energy_flow_hour_battery_charged": {
"name": "Battery energy charged this hour"
},
"energy_flow_hour_battery_discharged": {
"name": "Battery energy discharged this hour"
},
"energy_flow_hour_battery_source_grid": {
"name": "Battery charged from grid this hour"
},
"energy_flow_hour_battery_source_load": {
"name": "Battery charged from load this hour"
},
"energy_flow_hour_grid_exported": {
"name": "Energy exported to grid this hour"
},
"energy_flow_hour_grid_imported": {
"name": "Energy imported from grid this hour"
},
"energy_flow_hour_load_consumed": {
"name": "Load energy consumed this hour"
},
"energy_flow_hour_load_generated": {
"name": "Load energy generated this hour"
},
"energy_flow_hour_load_source_battery": {
"name": "Load supplied by battery this hour"
},
"energy_flow_hour_load_source_grid": {
"name": "Load supplied by grid this hour"
},
"energy_flow_month_battery_charged": {
"name": "Battery energy charged this month"
},
"energy_flow_month_battery_discharged": {
"name": "Battery energy discharged this month"
},
"energy_flow_month_battery_source_grid": {
"name": "Battery charged from grid this month"
},
"energy_flow_month_battery_source_load": {
"name": "Battery charged from load this month"
},
"energy_flow_month_grid_exported": {
"name": "Energy exported to grid this month"
},
"energy_flow_month_grid_imported": {
"name": "Energy imported from grid this month"
},
"energy_flow_month_grid_source_battery": {
"name": "Grid export from battery this month"
},
"energy_flow_month_grid_source_load": {
"name": "Grid export from load this month"
},
"energy_flow_month_load_consumed": {
"name": "Load energy consumed this month"
},
"energy_flow_month_load_generated": {
"name": "Load energy generated this month"
},
"energy_flow_month_load_source_battery": {
"name": "Load supplied by battery this month"
},
"energy_flow_month_load_source_grid": {
"name": "Load supplied by grid this month"
},
"estimated_hour_consumption": {
"name": "Estimated consumption current hour"
},
@@ -171,27 +97,6 @@
"power_factor": {
"name": "Power factor"
},
"power_flow_battery": {
"name": "Battery power"
},
"power_flow_from_grid": {
"name": "Power flow from grid"
},
"power_flow_from_load": {
"name": "Power flow from load"
},
"power_flow_grid": {
"name": "Grid power"
},
"power_flow_load": {
"name": "Load power"
},
"power_flow_to_grid": {
"name": "Power flow to grid"
},
"power_flow_to_load": {
"name": "Power flow to load"
},
"power_production": {
"name": "Power production"
},
@@ -201,15 +106,6 @@
"signal_strength": {
"name": "Signal strength"
},
"storage_available_energy": {
"name": "Available energy"
},
"storage_rated_capacity": {
"name": "Rated capacity"
},
"storage_rated_power": {
"name": "Rated power"
},
"storage_state_of_charge": {
"name": "State of charge"
},

View File

@@ -165,7 +165,7 @@ class DeviceListener(SharingDeviceListener):
self,
device: CustomerDevice,
updated_status_properties: list[str] | None = None,
dp_timestamps: dict[str, int] | None = None,
dp_timestamps: dict | None = None,
) -> None:
"""Update device status with optional DP timestamps."""
LOGGER.debug(

View File

@@ -471,11 +471,9 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

View File

@@ -57,7 +57,7 @@ class TuyaEntity(Entity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
self.async_write_ha_state()

View File

@@ -218,10 +218,10 @@ class TuyaEventEntity(TuyaEntity, EventEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
self.device, updated_status_properties
) or not (event_data := self._dpcode_wrapper.read_device_status(self.device)):
return

View File

@@ -31,10 +31,7 @@ class DeviceWrapper[T]:
options: list[str]
def skip_update(
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
self, device: CustomerDevice, updated_status_properties: list[str] | None
) -> bool:
"""Determine if the wrapper should skip an update.
@@ -65,10 +62,7 @@ class DPCodeWrapper(DeviceWrapper):
self.dpcode = dpcode
def skip_update(
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
self, device: CustomerDevice, updated_status_properties: list[str] | None
) -> bool:
"""Determine if the wrapper should skip an update.

View File

@@ -554,12 +554,10 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

View File

@@ -410,12 +410,10 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

View File

@@ -1853,11 +1853,9 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

View File

@@ -110,12 +110,10 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

View File

@@ -1043,12 +1043,10 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

View File

@@ -140,12 +140,10 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
dp_timestamps: dict | None = None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
return
self.async_write_ha_state()

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