mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 10:29:27 +00:00
Compare commits
1 Commits
copilot/fi
...
llm-python
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
176f9c9f94 |
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -653,7 +653,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.7.2
|
||||
uses: actions/dependency-review-action@v4.7.1
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
@@ -1341,7 +1341,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v5.5.0
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1491,7 +1491,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v5.5.0
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.29.10
|
||||
uses: github/codeql-action/init@v3.29.9
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.29.10
|
||||
uses: github/codeql-action/analyze@v3.29.9
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -422,8 +422,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/emby/ @mezz64
|
||||
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||
/tests/components/emoncms/ @borpin @alexandrecuer
|
||||
/homeassistant/components/emoncms_history/ @alexandrecuer
|
||||
/tests/components/emoncms_history/ @alexandrecuer
|
||||
/homeassistant/components/emonitor/ @bdraco
|
||||
/tests/components/emonitor/ @bdraco
|
||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||
|
||||
@@ -14,8 +14,5 @@ Still interested? Then you should take a peek at the [developer documentation](h
|
||||
|
||||
## Feature suggestions
|
||||
|
||||
If you want to suggest a new feature for Home Assistant (e.g. new integrations), please [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub.
|
||||
|
||||
## Issue Tracker
|
||||
|
||||
If you want to report an issue, please [create an issue](https://github.com/home-assistant/core/issues) on GitHub.
|
||||
If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests).
|
||||
We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests.
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"display_pm_standard": {
|
||||
"name": "Display PM standard",
|
||||
"state": {
|
||||
"ugm3": "μg/m³",
|
||||
"ugm3": "µg/m³",
|
||||
"us_aqi": "US AQI"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
@@ -64,9 +64,7 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
api = amberelectric.AmberApi(api_client)
|
||||
|
||||
try:
|
||||
sites: list[Site] = filter_sites(
|
||||
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
|
||||
)
|
||||
sites: list[Site] = filter_sites(api.get_sites())
|
||||
except amberelectric.ApiException as api_exception:
|
||||
if api_exception.status == 403:
|
||||
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
||||
|
||||
@@ -21,5 +21,3 @@ SERVICE_GET_FORECASTS = "get_forecasts"
|
||||
GENERAL_CHANNEL = "general"
|
||||
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
||||
FEED_IN_CHANNEL = "feed_in"
|
||||
|
||||
REQUEST_TIMEOUT = 15
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER, REQUEST_TIMEOUT
|
||||
from .const import LOGGER
|
||||
from .helpers import normalize_descriptor
|
||||
|
||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||
@@ -82,11 +82,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||
"grid": {},
|
||||
}
|
||||
try:
|
||||
data = self._api.get_current_prices(
|
||||
self.site_id,
|
||||
next=288,
|
||||
_request_timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
data = self._api.get_current_prices(self.site_id, next=288)
|
||||
intervals = [interval.actual_instance for interval in data]
|
||||
except ApiException as api_exception:
|
||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||
|
||||
@@ -449,6 +449,5 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
|
||||
return {
|
||||
"version": "home-assistant:1",
|
||||
"home_assistant": HA_VERSION,
|
||||
"devices": devices,
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
from .entity import APCUPSdEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -40,16 +40,22 @@ async def async_setup_entry(
|
||||
async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)])
|
||||
|
||||
|
||||
class OnlineStatus(APCUPSdEntity, BinarySensorEntity):
|
||||
class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
|
||||
"""Representation of a UPS online status."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: APCUPSdCoordinator,
|
||||
description: BinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the APCUPSd binary device."""
|
||||
super().__init__(coordinator, description)
|
||||
super().__init__(coordinator, context=description.key.upper())
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""Base entity for APCUPSd integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import APCUPSdCoordinator
|
||||
|
||||
|
||||
class APCUPSdEntity(CoordinatorEntity[APCUPSdCoordinator]):
|
||||
"""Base entity for APCUPSd integration."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: APCUPSdCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the APCUPSd entity."""
|
||||
super().__init__(coordinator, context=description.key.upper())
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
@@ -3,7 +3,10 @@ rules:
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
common-modules:
|
||||
status: done
|
||||
comment: |
|
||||
Consider deriving a base entity.
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
|
||||
@@ -23,10 +23,10 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import LAST_S_TEST
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
from .entity import APCUPSdEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -490,16 +490,22 @@ def infer_unit(value: str) -> tuple[str, str | None]:
|
||||
return value, None
|
||||
|
||||
|
||||
class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
||||
"""Representation of a sensor entity for APCUPSd status values."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: APCUPSdCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, description)
|
||||
super().__init__(coordinator=coordinator, context=description.key.upper())
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
# Initial update of attributes.
|
||||
self._update_attrs()
|
||||
|
||||
@@ -15,7 +15,6 @@ from asusrouter import AsusRouter, AsusRouterError
|
||||
from asusrouter.modules.client import AsusClient
|
||||
from asusrouter.modules.data import AsusData
|
||||
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
|
||||
from asusrouter.tools.connection import get_cookie_jar
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -26,7 +25,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
@@ -110,10 +109,7 @@ class AsusWrtBridge(ABC):
|
||||
) -> AsusWrtBridge:
|
||||
"""Get Bridge instance."""
|
||||
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
|
||||
session = async_create_clientsession(
|
||||
hass,
|
||||
cookie_jar=get_cookie_jar(),
|
||||
)
|
||||
session = async_get_clientsession(hass)
|
||||
return AsusWrtHttpBridge(conf, session)
|
||||
return AsusWrtLegacyBridge(conf, options)
|
||||
|
||||
|
||||
@@ -199,19 +199,23 @@ class AuthProvidersView(HomeAssistantView):
|
||||
)
|
||||
|
||||
|
||||
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
def _prepare_result_json(
|
||||
result: AuthFlowResult,
|
||||
) -> AuthFlowResult:
|
||||
"""Convert result to JSON."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return {
|
||||
key: val for key, val in result.items() if key not in ("result", "data")
|
||||
}
|
||||
data = result.copy()
|
||||
data.pop("result")
|
||||
data.pop("data")
|
||||
return data
|
||||
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
return result
|
||||
|
||||
data = dict(result)
|
||||
if (schema := result["data_schema"]) is None:
|
||||
data["data_schema"] = []
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
||||
@@ -149,16 +149,20 @@ def websocket_depose_mfa(
|
||||
hass.async_create_task(async_depose(msg))
|
||||
|
||||
|
||||
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
def _prepare_result_json(
|
||||
result: data_entry_flow.FlowResult,
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Convert result to JSON."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return dict(result)
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
return result.copy()
|
||||
|
||||
data = dict(result)
|
||||
if (schema := result["data_schema"]) is None:
|
||||
data["data_schema"] = []
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
return result
|
||||
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==1.0.1",
|
||||
"bleak-retry-connector==4.0.2",
|
||||
"bleak-retry-connector==4.0.1",
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||
"requirements": ["brother==5.0.1"],
|
||||
"requirements": ["brother==5.0.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_printer._tcp.local.",
|
||||
|
||||
@@ -72,7 +72,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
key=str(BTHomeExtendedSensorDeviceClass.CHANNEL),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Conductivity (μS/cm)
|
||||
# Conductivity (µS/cm)
|
||||
(
|
||||
BTHomeSensorDeviceClass.CONDUCTIVITY,
|
||||
Units.CONDUCTIVITY,
|
||||
@@ -215,7 +215,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# PM10 (μg/m3)
|
||||
# PM10 (µg/m3)
|
||||
(
|
||||
BTHomeSensorDeviceClass.PM10,
|
||||
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -225,7 +225,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# PM2.5 (μg/m3)
|
||||
# PM2.5 (µg/m3)
|
||||
(
|
||||
BTHomeSensorDeviceClass.PM25,
|
||||
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -318,7 +318,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
key=str(BTHomeSensorDeviceClass.UV_INDEX),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Volatile organic Compounds (VOC) (μg/m3)
|
||||
# Volatile organic Compounds (VOC) (µg/m3)
|
||||
(
|
||||
BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
|
||||
@@ -564,13 +564,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
This is used by cameras with CameraEntityFeature.STREAM
|
||||
and StreamType.HLS.
|
||||
"""
|
||||
# Check if camera has a go2rtc provider that can provide stream sources
|
||||
if (
|
||||
self._webrtc_provider
|
||||
and hasattr(self._webrtc_provider, 'async_get_stream_source')
|
||||
and self._webrtc_provider.domain == "go2rtc"
|
||||
):
|
||||
return await self._webrtc_provider.async_get_stream_source(self)
|
||||
return None
|
||||
|
||||
async def async_handle_async_webrtc_offer(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.9"],
|
||||
"requirements": ["PyChromecast==14.0.7"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -137,16 +137,20 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView):
|
||||
|
||||
def _prepare_config_flow_result_json(
|
||||
result: data_entry_flow.FlowResult,
|
||||
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
prepare_result_json: Callable[
|
||||
[data_entry_flow.FlowResult], data_entry_flow.FlowResult
|
||||
],
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Convert result to JSON."""
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return prepare_result_json(result)
|
||||
|
||||
data = {key: val for key, val in result.items() if key not in ("data", "context")}
|
||||
entry: config_entries.ConfigEntry = result["result"] # type: ignore[typeddict-item]
|
||||
data = result.copy()
|
||||
entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item]
|
||||
# We overwrite the ConfigEntry object with its json representation.
|
||||
data["result"] = entry.as_json_fragment
|
||||
data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key]
|
||||
data.pop("data")
|
||||
data.pop("context")
|
||||
return data
|
||||
|
||||
|
||||
@@ -200,8 +204,8 @@ class ConfigManagerFlowIndexView(
|
||||
|
||||
def _prepare_result_json(
|
||||
self, result: data_entry_flow.FlowResult
|
||||
) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Convert result to JSON."""
|
||||
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
|
||||
|
||||
|
||||
@@ -225,8 +229,8 @@ class ConfigManagerFlowResourceView(
|
||||
|
||||
def _prepare_result_json(
|
||||
self, result: data_entry_flow.FlowResult
|
||||
) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Convert result to JSON."""
|
||||
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.16.0"],
|
||||
"requirements": ["pydaikin==2.15.0"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -99,18 +99,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
config_entry, version=1, minor_version=3
|
||||
)
|
||||
|
||||
if config_entry.minor_version < 4:
|
||||
# Ensure we use the correct units
|
||||
new_options = {**config_entry.options}
|
||||
|
||||
if new_options.get("unit_prefix") == "\u00b5":
|
||||
# Ensure we use the preferred coding of μ
|
||||
new_options["unit_prefix"] = "\u03bc"
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options=new_options, version=1, minor_version=4
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to configuration version %s.%s successful",
|
||||
config_entry.version,
|
||||
|
||||
@@ -36,7 +36,7 @@ from .const import (
|
||||
|
||||
UNIT_PREFIXES = [
|
||||
selector.SelectOptionDict(value="n", label="n (nano)"),
|
||||
selector.SelectOptionDict(value="μ", label="μ (micro)"),
|
||||
selector.SelectOptionDict(value="µ", label="µ (micro)"),
|
||||
selector.SelectOptionDict(value="m", label="m (milli)"),
|
||||
selector.SelectOptionDict(value="k", label="k (kilo)"),
|
||||
selector.SelectOptionDict(value="M", label="M (mega)"),
|
||||
@@ -142,7 +142,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
options_flow = OPTIONS_FLOW
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 4
|
||||
MINOR_VERSION = 3
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
||||
@@ -63,7 +63,7 @@ ATTR_SOURCE_ID = "source"
|
||||
UNIT_PREFIXES = {
|
||||
None: 1,
|
||||
"n": 1e-9,
|
||||
"μ": 1e-6,
|
||||
"µ": 1e-6,
|
||||
"m": 1e-3,
|
||||
"k": 1e3,
|
||||
"M": 1e6,
|
||||
|
||||
@@ -157,7 +157,7 @@ SENSORS: dict[str | None, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"μg/m³": SensorEntityDescription(
|
||||
"µg/m³": SensorEntityDescription(
|
||||
key="concentration|microgram_per_cubic_meter",
|
||||
translation_key="concentration",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Support for sending data to Emoncms."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from pyemoncms import EmoncmsClient
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -18,9 +17,9 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,51 +42,61 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_send_to_emoncms(
|
||||
hass: HomeAssistant,
|
||||
emoncms_client: EmoncmsClient,
|
||||
whitelist: list[str],
|
||||
node: str | int,
|
||||
_: datetime,
|
||||
) -> None:
|
||||
"""Send data to Emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
for entity_id in whitelist:
|
||||
state = hass.states.get(entity_id)
|
||||
if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
|
||||
continue
|
||||
try:
|
||||
payload_dict[entity_id] = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if payload_dict:
|
||||
try:
|
||||
await emoncms_client.async_input_post(data=payload_dict, node=node)
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOGGER.warning("Network error when sending data to Emoncms: %s", err)
|
||||
except ValueError as err:
|
||||
_LOGGER.warning("Value error when preparing data for Emoncms: %s", err)
|
||||
else:
|
||||
_LOGGER.debug("Sent data to Emoncms: %s", payload_dict)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Emoncms history component."""
|
||||
conf = config[DOMAIN]
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
input_node = str(conf.get(CONF_INPUTNODE))
|
||||
|
||||
emoncms_client = EmoncmsClient(
|
||||
url=conf.get(CONF_URL),
|
||||
api_key=conf.get(CONF_API_KEY),
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
async_track_time_interval(
|
||||
hass,
|
||||
partial(async_send_to_emoncms, hass, emoncms_client, whitelist, input_node),
|
||||
timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)),
|
||||
)
|
||||
def send_data(url, apikey, node, payload):
|
||||
"""Send payload data to Emoncms."""
|
||||
try:
|
||||
fullurl = f"{url}/input/post.json"
|
||||
data = {"apikey": apikey, "data": payload}
|
||||
parameters = {"node": node}
|
||||
req = requests.post(
|
||||
fullurl, params=parameters, data=data, allow_redirects=True, timeout=5
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl)
|
||||
|
||||
else:
|
||||
if req.status_code != HTTPStatus.OK:
|
||||
_LOGGER.error(
|
||||
"Error saving data %s to %s (http status code = %d)",
|
||||
payload,
|
||||
fullurl,
|
||||
req.status_code,
|
||||
)
|
||||
|
||||
def update_emoncms(time):
|
||||
"""Send whitelisted entities states regularly to Emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
for entity_id in whitelist:
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
|
||||
continue
|
||||
|
||||
try:
|
||||
payload_dict[entity_id] = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if payload_dict:
|
||||
payload = ",".join(f"{key}:{val}" for key, val in payload_dict.items())
|
||||
|
||||
send_data(
|
||||
conf.get(CONF_URL),
|
||||
conf.get(CONF_API_KEY),
|
||||
str(conf.get(CONF_INPUTNODE)),
|
||||
f"{{{payload}}}",
|
||||
)
|
||||
|
||||
track_point_in_time(
|
||||
hass, update_emoncms, time + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))
|
||||
)
|
||||
|
||||
update_emoncms(dt_util.utcnow())
|
||||
return True
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"domain": "emoncms_history",
|
||||
"name": "Emoncms History",
|
||||
"codeowners": ["@alexandrecuer"],
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/emoncms_history",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyemoncms==0.1.2"]
|
||||
"quality_scale": "legacy"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.8.0"
|
||||
STABLE_BLE_VERSION_STR = "2025.5.0"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/foscam",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["libpyfoscamcgi"],
|
||||
"requirements": ["libpyfoscamcgi==0.0.7"]
|
||||
"requirements": ["libpyfoscamcgi==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
FreeboxAlarm(router, node)
|
||||
FreeboxAlarm(hass, router, node)
|
||||
for node in router.home_devices.values()
|
||||
if node["category"] == FreeboxHomeCategory.ALARM
|
||||
),
|
||||
@@ -49,9 +49,11 @@ class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity):
|
||||
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize an alarm."""
|
||||
super().__init__(router, node)
|
||||
super().__init__(hass, router, node)
|
||||
|
||||
# Commands
|
||||
self._command_trigger = self.get_command_id(
|
||||
|
||||
@@ -50,12 +50,12 @@ async def async_setup_entry(
|
||||
|
||||
for node in router.home_devices.values():
|
||||
if node["category"] == FreeboxHomeCategory.PIR:
|
||||
binary_entities.append(FreeboxPirSensor(router, node))
|
||||
binary_entities.append(FreeboxPirSensor(hass, router, node))
|
||||
elif node["category"] == FreeboxHomeCategory.DWS:
|
||||
binary_entities.append(FreeboxDwsSensor(router, node))
|
||||
binary_entities.append(FreeboxDwsSensor(hass, router, node))
|
||||
|
||||
binary_entities.extend(
|
||||
FreeboxCoverSensor(router, node)
|
||||
FreeboxCoverSensor(hass, router, node)
|
||||
for endpoint in node["show_endpoints"]
|
||||
if (
|
||||
endpoint["name"] == "cover"
|
||||
@@ -74,12 +74,13 @@ class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
router: FreeboxRouter,
|
||||
node: dict[str, Any],
|
||||
sub_node: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize a Freebox binary sensor."""
|
||||
super().__init__(router, node, sub_node)
|
||||
super().__init__(hass, router, node, sub_node)
|
||||
self._command_id = self.get_command_id(
|
||||
node["type"]["endpoints"], "signal", self._sensor_name
|
||||
)
|
||||
@@ -122,7 +123,9 @@ class FreeboxCoverSensor(FreeboxHomeBinarySensor):
|
||||
|
||||
_sensor_name = "cover"
|
||||
|
||||
def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize a cover for another device."""
|
||||
cover_node = next(
|
||||
filter(
|
||||
@@ -131,7 +134,7 @@ class FreeboxCoverSensor(FreeboxHomeBinarySensor):
|
||||
),
|
||||
None,
|
||||
)
|
||||
super().__init__(router, node, cover_node)
|
||||
super().__init__(hass, router, node, cover_node)
|
||||
|
||||
|
||||
class FreeboxRaidDegradedSensor(BinarySensorEntity):
|
||||
|
||||
@@ -74,7 +74,7 @@ class FreeboxCamera(FreeboxHomeEntity, FFmpegCamera):
|
||||
) -> None:
|
||||
"""Initialize a camera."""
|
||||
|
||||
super().__init__(router, node)
|
||||
super().__init__(hass, router, node)
|
||||
device_info = {
|
||||
CONF_NAME: node["label"].strip(),
|
||||
CONF_INPUT: node["props"]["Stream"],
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -20,11 +22,13 @@ class FreeboxHomeEntity(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
router: FreeboxRouter,
|
||||
node: dict[str, Any],
|
||||
sub_node: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize a Freebox Home entity."""
|
||||
self._hass = hass
|
||||
self._router = router
|
||||
self._node = node
|
||||
self._sub_node = sub_node
|
||||
@@ -40,6 +44,7 @@ class FreeboxHomeEntity(Entity):
|
||||
self._available = True
|
||||
self._firmware = node["props"].get("FwVersion")
|
||||
self._manufacturer = "Freebox SAS"
|
||||
self._remove_signal_update: Callable[[], None] | None = None
|
||||
|
||||
self._model = CATEGORY_TO_MODEL.get(node["category"])
|
||||
if self._model is None:
|
||||
@@ -56,7 +61,10 @@ class FreeboxHomeEntity(Entity):
|
||||
model=self._model,
|
||||
name=self._device_name,
|
||||
sw_version=self._firmware,
|
||||
via_device=(DOMAIN, router.mac),
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
router.mac,
|
||||
),
|
||||
)
|
||||
|
||||
async def async_update_signal(self) -> None:
|
||||
@@ -108,14 +116,23 @@ class FreeboxHomeEntity(Entity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback."""
|
||||
self.async_on_remove(
|
||||
self.remove_signal_update(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self._hass,
|
||||
self._router.signal_home_device_update,
|
||||
self.async_update_signal,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""When entity will be removed from hass."""
|
||||
if self._remove_signal_update is not None:
|
||||
self._remove_signal_update()
|
||||
|
||||
def remove_signal_update(self, dispatcher: Callable[[], None]) -> None:
|
||||
"""Register state update callback."""
|
||||
self._remove_signal_update = dispatcher
|
||||
|
||||
def get_value(self, ep_type: str, name: str):
|
||||
"""Get the value."""
|
||||
node = next(
|
||||
|
||||
@@ -68,6 +68,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
router = entry.runtime_data
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - %s - %s temperature sensors",
|
||||
@@ -75,7 +76,7 @@ async def async_setup_entry(
|
||||
router.mac,
|
||||
len(router.sensors_temperature),
|
||||
)
|
||||
entities: list[SensorEntity] = [
|
||||
entities = [
|
||||
FreeboxSensor(
|
||||
router,
|
||||
SensorEntityDescription(
|
||||
@@ -104,16 +105,14 @@ async def async_setup_entry(
|
||||
for description in DISK_PARTITION_SENSORS
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
FreeboxBatterySensor(router, node, endpoint)
|
||||
for node in router.home_devices.values()
|
||||
for endpoint in node["show_endpoints"]
|
||||
if (
|
||||
endpoint["name"] == "battery"
|
||||
and endpoint["ep_type"] == "signal"
|
||||
and endpoint.get("value") is not None
|
||||
)
|
||||
)
|
||||
for node in router.home_devices.values():
|
||||
for endpoint in node["show_endpoints"]:
|
||||
if (
|
||||
endpoint["name"] == "battery"
|
||||
and endpoint["ep_type"] == "signal"
|
||||
and endpoint.get("value") is not None
|
||||
):
|
||||
entities.append(FreeboxBatterySensor(hass, router, node, endpoint))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, True)
|
||||
|
||||
@@ -50,7 +50,7 @@ CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5"
|
||||
CONF_FRONTEND_REPO = "development_repo"
|
||||
CONF_JS_VERSION = "javascript_version"
|
||||
|
||||
DEFAULT_THEME_COLOR = "#2980b9"
|
||||
DEFAULT_THEME_COLOR = "#03A9F4"
|
||||
|
||||
|
||||
DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels")
|
||||
|
||||
@@ -300,13 +300,6 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
camera.entity_id, width, height
|
||||
)
|
||||
|
||||
async def async_get_stream_source(self, camera: Camera) -> str | None:
|
||||
"""Get a stream source URL suitable for recording."""
|
||||
await self._update_stream_source(camera)
|
||||
# Return an HLS stream URL that can be used by the stream component for recording
|
||||
# go2rtc provides HLS streams at /api/stream.m3u8?src=<stream_name>
|
||||
return f"{self._url}api/stream.m3u8?src={camera.entity_id}"
|
||||
|
||||
async def _update_stream_source(self, camera: Camera) -> None:
|
||||
"""Update the stream source in go2rtc config if needed."""
|
||||
if not (stream_source := await camera.stream_source()):
|
||||
|
||||
@@ -61,19 +61,18 @@ PLACEHOLDER_KEY_REASON = "reason"
|
||||
|
||||
UNSUPPORTED_REASONS = {
|
||||
"apparmor",
|
||||
"cgroup_version",
|
||||
"connectivity_check",
|
||||
"content_trust",
|
||||
"dbus",
|
||||
"dns_server",
|
||||
"docker_configuration",
|
||||
"docker_version",
|
||||
"cgroup_version",
|
||||
"job_conditions",
|
||||
"lxc",
|
||||
"network_manager",
|
||||
"os",
|
||||
"os_agent",
|
||||
"os_version",
|
||||
"restart_policy",
|
||||
"software",
|
||||
"source_mods",
|
||||
@@ -81,7 +80,6 @@ UNSUPPORTED_REASONS = {
|
||||
"systemd",
|
||||
"systemd_journal",
|
||||
"systemd_resolved",
|
||||
"virtualization_image",
|
||||
}
|
||||
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
|
||||
# provides no additional information beyond the unhealthy one then skip that repair.
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.3.2b0"],
|
||||
"requirements": ["aiohasupervisor==0.3.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.79", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.78", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ class NitrogenDioxideSensor(AirQualitySensor):
|
||||
class VolatileOrganicCompoundsSensor(AirQualitySensor):
|
||||
"""Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor.
|
||||
|
||||
Sensor entity must return VOC in μg/m3.
|
||||
Sensor entity must return VOC in µg/m3.
|
||||
"""
|
||||
|
||||
def create_services(self) -> None:
|
||||
|
||||
@@ -494,7 +494,7 @@ def temperature_to_states(temperature: float, unit: str) -> float:
|
||||
|
||||
|
||||
def density_to_air_quality(density: float) -> int:
|
||||
"""Map PM2.5 μg/m3 density to HomeKit AirQuality level."""
|
||||
"""Map PM2.5 µg/m3 density to HomeKit AirQuality level."""
|
||||
if density <= 9: # US AQI 0-50 (HomeKit: Excellent)
|
||||
return 1
|
||||
if density <= 35.4: # US AQI 51-100 (HomeKit: Good)
|
||||
@@ -507,7 +507,7 @@ def density_to_air_quality(density: float) -> int:
|
||||
|
||||
|
||||
def density_to_air_quality_pm10(density: float) -> int:
|
||||
"""Map PM10 μg/m3 density to HomeKit AirQuality level."""
|
||||
"""Map PM10 µg/m3 density to HomeKit AirQuality level."""
|
||||
if density <= 54: # US AQI 0-50 (HomeKit: Excellent)
|
||||
return 1
|
||||
if density <= 154: # US AQI 51-100 (HomeKit: Good)
|
||||
@@ -520,7 +520,7 @@ def density_to_air_quality_pm10(density: float) -> int:
|
||||
|
||||
|
||||
def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
|
||||
"""Map nitrogen dioxide μg/m3 to HomeKit AirQuality level."""
|
||||
"""Map nitrogen dioxide µg/m3 to HomeKit AirQuality level."""
|
||||
if density <= 30:
|
||||
return 1
|
||||
if density <= 60:
|
||||
@@ -533,7 +533,7 @@ def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
|
||||
|
||||
|
||||
def density_to_air_quality_voc(density: float) -> int:
|
||||
"""Map VOCs μg/m3 to HomeKit AirQuality level.
|
||||
"""Map VOCs µg/m3 to HomeKit AirQuality level.
|
||||
|
||||
The VOC mappings use the IAQ guidelines for Europe released by the WHO (World Health Organization).
|
||||
Referenced from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.2.1"]
|
||||
"requirements": ["automower-ble==0.2.7"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .account import IcloudAccount, IcloudConfigEntry
|
||||
from .account import IcloudAccount
|
||||
from .const import (
|
||||
ATTR_ACCOUNT,
|
||||
ATTR_DEVICE_NAME,
|
||||
@@ -92,10 +92,8 @@ def lost_device(service: ServiceCall) -> None:
|
||||
def update_account(service: ServiceCall) -> None:
|
||||
"""Call the update function of an iCloud account."""
|
||||
if (account := service.data.get(ATTR_ACCOUNT)) is None:
|
||||
# Update all accounts when no specific account is provided
|
||||
entry: IcloudConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
entry.runtime_data.keep_alive()
|
||||
for account in service.hass.data[DOMAIN].values():
|
||||
account.keep_alive()
|
||||
else:
|
||||
_get_account(service.hass, account).keep_alive()
|
||||
|
||||
@@ -104,12 +102,17 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount:
|
||||
if account_identifier is None:
|
||||
return None
|
||||
|
||||
entry: IcloudConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if entry.runtime_data.username == account_identifier:
|
||||
return entry.runtime_data
|
||||
icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier)
|
||||
if icloud_account is None:
|
||||
for account in hass.data[DOMAIN].values():
|
||||
if account.username == account_identifier:
|
||||
icloud_account = account
|
||||
|
||||
raise ValueError(f"No iCloud account with username or name {account_identifier}")
|
||||
if icloud_account is None:
|
||||
raise ValueError(
|
||||
f"No iCloud account with username or name {account_identifier}"
|
||||
)
|
||||
return icloud_account
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -7,26 +7,3 @@ TIMEOUT = 30
|
||||
PLATFORMS = [
|
||||
Platform.SENSOR,
|
||||
]
|
||||
ATTR_BATTERY_STATUS = ["charging", "discharging", "charged"]
|
||||
ATTR_INVERTER_STATE = [
|
||||
"unsynchronized",
|
||||
"grid_consumption",
|
||||
"grid_injection",
|
||||
"grid_synchronised_but_not_used",
|
||||
]
|
||||
ATTR_TIMELINE_STATUS = [
|
||||
"com_lost",
|
||||
"warning_grid",
|
||||
"warning_pv",
|
||||
"warning_bat",
|
||||
"error_ond",
|
||||
"error_soft",
|
||||
"error_pv",
|
||||
"error_grid",
|
||||
"error_bat",
|
||||
"good_1",
|
||||
"info_soft",
|
||||
"info_ond",
|
||||
"info_bat",
|
||||
"info_smartlo",
|
||||
]
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import TIMEOUT
|
||||
|
||||
HUBNAME = "imeon_inverter_hub"
|
||||
INTERVAL = 60
|
||||
INTERVAL = timedelta(seconds=60)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
|
||||
@@ -44,7 +44,7 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=HUBNAME,
|
||||
update_interval=timedelta(seconds=INTERVAL),
|
||||
update_interval=INTERVAL,
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
|
||||
# Fetch data using distant API
|
||||
try:
|
||||
await self._api.update()
|
||||
except (ValueError, TimeoutError, ClientError) as e:
|
||||
except (ValueError, ClientError) as e:
|
||||
raise UpdateFailed(e) from e
|
||||
|
||||
# Store data
|
||||
|
||||
@@ -7,14 +7,8 @@
|
||||
"battery_soc": {
|
||||
"default": "mdi:battery-charging-100"
|
||||
},
|
||||
"battery_status": {
|
||||
"default": "mdi:battery-alert"
|
||||
},
|
||||
"battery_stored": {
|
||||
"default": "mdi:battery-arrow-up"
|
||||
},
|
||||
"battery_consumed": {
|
||||
"default": "mdi:battery-arrow-down"
|
||||
"default": "mdi:battery"
|
||||
},
|
||||
"grid_current_l1": {
|
||||
"default": "mdi:current-ac"
|
||||
@@ -56,7 +50,7 @@
|
||||
"default": "mdi:power-socket"
|
||||
},
|
||||
"meter_power": {
|
||||
"default": "mdi:meter-electric"
|
||||
"default": "mdi:power-plug"
|
||||
},
|
||||
"output_current_l1": {
|
||||
"default": "mdi:current-ac"
|
||||
@@ -122,7 +116,7 @@
|
||||
"default": "mdi:home-lightning-bolt"
|
||||
},
|
||||
"monitoring_minute_grid_consumption": {
|
||||
"default": "mdi:transmission-tower-import"
|
||||
"default": "mdi:transmission-tower"
|
||||
},
|
||||
"monitoring_minute_grid_injection": {
|
||||
"default": "mdi:transmission-tower-export"
|
||||
@@ -132,43 +126,6 @@
|
||||
},
|
||||
"monitoring_minute_solar_production": {
|
||||
"default": "mdi:solar-power"
|
||||
},
|
||||
"timeline_type_msg": {
|
||||
"default": "mdi:check-circle",
|
||||
"state": {
|
||||
"com_lost": "mdi:lan-disconnect",
|
||||
"warning_grid": "mdi:alert-circle",
|
||||
"warning_pv": "mdi:alert-circle",
|
||||
"warning_bat": "mdi:alert-circle",
|
||||
"error_ond": "mdi:close-octagon",
|
||||
"error_soft": "mdi:close-octagon",
|
||||
"error_pv": "mdi:close-octagon",
|
||||
"error_grid": "mdi:close-octagon",
|
||||
"error_bat": "mdi:close-octagon",
|
||||
"good_1": "mdi:check-circle",
|
||||
"info_soft": "mdi:information-slab-circle",
|
||||
"info_ond": "mdi:information-slab-circle",
|
||||
"info_bat": "mdi:information-slab-circle",
|
||||
"info_smartlo": "mdi:information-slab-circle"
|
||||
}
|
||||
},
|
||||
"energy_pv": {
|
||||
"default": "mdi:solar-power"
|
||||
},
|
||||
"energy_grid_injected": {
|
||||
"default": "mdi:transmission-tower-export"
|
||||
},
|
||||
"energy_grid_consumed": {
|
||||
"default": "mdi:transmission-tower-import"
|
||||
},
|
||||
"energy_building_consumption": {
|
||||
"default": "mdi:home-lightning-bolt-outline"
|
||||
},
|
||||
"energy_battery_stored": {
|
||||
"default": "mdi:battery-arrow-up-outline"
|
||||
},
|
||||
"energy_battery_consumed": {
|
||||
"default": "mdi:battery-arrow-down-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import ATTR_BATTERY_STATUS, ATTR_INVERTER_STATE, ATTR_TIMELINE_STATUS
|
||||
from .coordinator import InverterCoordinator
|
||||
from .entity import InverterEntity
|
||||
|
||||
@@ -48,24 +47,11 @@ SENSOR_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="battery_status",
|
||||
translation_key="battery_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=ATTR_BATTERY_STATUS,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="battery_stored",
|
||||
translation_key="battery_stored",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="battery_consumed",
|
||||
translation_key="battery_consumed",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Grid
|
||||
@@ -162,12 +148,6 @@ SENSOR_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="manager_inverter_state",
|
||||
translation_key="manager_inverter_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=ATTR_INVERTER_STATE,
|
||||
),
|
||||
# Meter
|
||||
SensorEntityDescription(
|
||||
key="meter_power",
|
||||
@@ -258,16 +238,16 @@ SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescription(
|
||||
key="pv_consumed",
|
||||
translation_key="pv_consumed",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="pv_injected",
|
||||
translation_key="pv_injected",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="pv_power_1",
|
||||
@@ -310,14 +290,14 @@ SENSOR_DESCRIPTIONS = (
|
||||
key="monitoring_self_consumption",
|
||||
translation_key="monitoring_self_consumption",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="monitoring_self_sufficiency",
|
||||
translation_key="monitoring_self_sufficiency",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
# Monitoring (instant minute data)
|
||||
@@ -361,62 +341,6 @@ SENSOR_DESCRIPTIONS = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
# Timeline
|
||||
SensorEntityDescription(
|
||||
key="timeline_type_msg",
|
||||
translation_key="timeline_type_msg",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=ATTR_TIMELINE_STATUS,
|
||||
),
|
||||
# Daily energy counters
|
||||
SensorEntityDescription(
|
||||
key="energy_pv",
|
||||
translation_key="energy_pv",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_grid_injected",
|
||||
translation_key="energy_grid_injected",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_grid_consumed",
|
||||
translation_key="energy_grid_consumed",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_building_consumption",
|
||||
translation_key="energy_building_consumption",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_battery_stored",
|
||||
translation_key="energy_battery_stored",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_battery_consumed",
|
||||
translation_key="energy_battery_consumed",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,20 +35,9 @@
|
||||
"battery_soc": {
|
||||
"name": "Battery state of charge"
|
||||
},
|
||||
"battery_status": {
|
||||
"name": "Battery status",
|
||||
"state": {
|
||||
"charged": "Charged",
|
||||
"charging": "[%key:common::state::charging%]",
|
||||
"discharging": "[%key:common::state::discharging%]"
|
||||
}
|
||||
},
|
||||
"battery_stored": {
|
||||
"name": "Battery stored"
|
||||
},
|
||||
"battery_consumed": {
|
||||
"name": "Battery consumed"
|
||||
},
|
||||
"grid_current_l1": {
|
||||
"name": "Grid current L1"
|
||||
},
|
||||
@@ -88,15 +77,6 @@
|
||||
"inverter_injection_power_limit": {
|
||||
"name": "Injection power limit"
|
||||
},
|
||||
"manager_inverter_state": {
|
||||
"name": "Inverter state",
|
||||
"state": {
|
||||
"unsynchronized": "Unsynchronized",
|
||||
"grid_consumption": "Grid consumption",
|
||||
"grid_injection": "Grid injection",
|
||||
"grid_synchronised_but_not_used": "Grid unsynchronized but used"
|
||||
}
|
||||
},
|
||||
"meter_power": {
|
||||
"name": "Meter power"
|
||||
},
|
||||
@@ -155,63 +135,25 @@
|
||||
"name": "Component temperature"
|
||||
},
|
||||
"monitoring_self_consumption": {
|
||||
"name": "Self-consumption"
|
||||
"name": "Monitoring self-consumption"
|
||||
},
|
||||
"monitoring_self_sufficiency": {
|
||||
"name": "Self-sufficiency"
|
||||
"name": "Monitoring self-sufficiency"
|
||||
},
|
||||
"monitoring_minute_building_consumption": {
|
||||
"name": "Building consumption"
|
||||
"name": "Monitoring building consumption (minute)"
|
||||
},
|
||||
"monitoring_minute_grid_consumption": {
|
||||
"name": "Grid consumption"
|
||||
"name": "Monitoring grid consumption (minute)"
|
||||
},
|
||||
"monitoring_minute_grid_injection": {
|
||||
"name": "Grid injection"
|
||||
"name": "Monitoring grid injection (minute)"
|
||||
},
|
||||
"monitoring_minute_grid_power_flow": {
|
||||
"name": "Grid power flow"
|
||||
"name": "Monitoring grid power flow (minute)"
|
||||
},
|
||||
"monitoring_minute_solar_production": {
|
||||
"name": "Solar production"
|
||||
},
|
||||
"timeline_type_msg": {
|
||||
"name": "Timeline status",
|
||||
"state": {
|
||||
"com_lost": "Communication lost.",
|
||||
"warning_grid": "Power grid warning detected.",
|
||||
"warning_pv": "PV system warning detected.",
|
||||
"warning_bat": "Battery warning detected.",
|
||||
"error_ond": "Inverter error detected.",
|
||||
"error_soft": "Software error detected.",
|
||||
"error_pv": "PV system error detected.",
|
||||
"error_grid": "Power grid error detected.",
|
||||
"error_bat": "Battery error detected.",
|
||||
"good_1": "System operating normally.",
|
||||
"web_account": "Web account notification.",
|
||||
"info_soft": "Software information available.",
|
||||
"info_ond": "Inverter information available.",
|
||||
"info_bat": "Battery information available.",
|
||||
"info_smartlo": "Smart load information available."
|
||||
}
|
||||
},
|
||||
"energy_pv": {
|
||||
"name": "Today PV energy"
|
||||
},
|
||||
"energy_grid_injected": {
|
||||
"name": "Today grid-injected energy"
|
||||
},
|
||||
"energy_grid_consumed": {
|
||||
"name": "Today grid-consumed energy"
|
||||
},
|
||||
"energy_building_consumption": {
|
||||
"name": "Today building consumption"
|
||||
},
|
||||
"energy_battery_stored": {
|
||||
"name": "Today battery-stored energy"
|
||||
},
|
||||
"energy_battery_consumed": {
|
||||
"name": "Today battery-consumed energy"
|
||||
"name": "Monitoring solar production (minute)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["imgw_pib==1.5.4"]
|
||||
"requirements": ["imgw_pib==1.5.3"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["mastodon"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["Mastodon.py==2.1.1"]
|
||||
"requirements": ["Mastodon.py==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ rules:
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
dependency-transparency:
|
||||
status: todo
|
||||
comment: |
|
||||
Mastodon.py does not have CI build/publish.
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
|
||||
@@ -396,7 +396,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="DishwasherAlarmDoorError",
|
||||
translation_key="alarm_door",
|
||||
translation_key="dishwasher_alarm_door",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: (
|
||||
@@ -407,19 +407,4 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(clusters.DishwasherAlarm.Attributes.State,),
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="RefrigeratorAlarmDoorOpen",
|
||||
translation_key="alarm_door",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: (
|
||||
x == clusters.RefrigeratorAlarm.Bitmaps.AlarmBitmap.kDoorOpen
|
||||
),
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.RefrigeratorAlarm.Attributes.State,),
|
||||
allow_multi=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -57,15 +57,6 @@
|
||||
"current_phase": {
|
||||
"default": "mdi:state-machine"
|
||||
},
|
||||
"eve_weather_trend": {
|
||||
"default": "mdi:weather",
|
||||
"state": {
|
||||
"sunny": "mdi:weather-sunny",
|
||||
"cloudy": "mdi:weather-cloudy",
|
||||
"rainy": "mdi:weather-rainy",
|
||||
"stormy": "mdi:weather-windy"
|
||||
}
|
||||
},
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
|
||||
@@ -70,14 +70,6 @@ CONTAMINATION_STATE_MAP = {
|
||||
clusters.SmokeCoAlarm.Enums.ContaminationStateEnum.kCritical: "critical",
|
||||
}
|
||||
|
||||
EVE_CLUSTER_WEATHER_MAP = {
|
||||
# enum with known Weather state values which we can translate
|
||||
1: "sunny",
|
||||
3: "cloudy",
|
||||
6: "rainy",
|
||||
14: "stormy",
|
||||
}
|
||||
|
||||
OPERATIONAL_STATE_MAP = {
|
||||
# enum with known Operation state values which we can translate
|
||||
clusters.OperationalState.Enums.OperationalStateEnum.kStopped: "stopped",
|
||||
@@ -525,19 +517,6 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(EveCluster.Attributes.Pressure,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="EveWeatherWeatherTrend",
|
||||
translation_key="eve_weather_trend",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
native_unit_of_measurement=None,
|
||||
options=[x for x in EVE_CLUSTER_WEATHER_MAP.values() if x is not None],
|
||||
device_to_ha=EVE_CLUSTER_WEATHER_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(EveCluster.Attributes.WeatherTrend,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"dishwasher_alarm_inflow": {
|
||||
"name": "Inflow alarm"
|
||||
},
|
||||
"alarm_door": {
|
||||
"dishwasher_alarm_door": {
|
||||
"name": "Door alarm"
|
||||
}
|
||||
},
|
||||
@@ -434,15 +434,6 @@
|
||||
"pump_speed": {
|
||||
"name": "Rotation speed"
|
||||
},
|
||||
"eve_weather_trend": {
|
||||
"name": "Weather trend",
|
||||
"state": {
|
||||
"cloudy": "Cloudy",
|
||||
"rainy": "Rainy",
|
||||
"sunny": "Sunny",
|
||||
"stormy": "Stormy"
|
||||
}
|
||||
},
|
||||
"evse_circuit_capacity": {
|
||||
"name": "Circuit capacity"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2025.08.11"],
|
||||
"requirements": ["yt-dlp[default]==2025.07.21"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -10,12 +10,9 @@ from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SNOWY_RAINY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CLOUD_COVERAGE,
|
||||
ATTR_FORECAST_HUMIDITY,
|
||||
ATTR_FORECAST_NATIVE_PRESSURE,
|
||||
ATTR_FORECAST_NATIVE_TEMP,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
@@ -37,9 +34,6 @@ FORECAST_MAP = {
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: "templow",
|
||||
ATTR_FORECAST_WIND_BEARING: "wind_bearing",
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed",
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "wind_gust",
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: "cloudiness",
|
||||
ATTR_FORECAST_HUMIDITY: "humidity",
|
||||
}
|
||||
|
||||
CONDITION_MAP = {
|
||||
|
||||
@@ -138,16 +138,6 @@ class MetEireannWeather(
|
||||
"""Return the wind direction."""
|
||||
return self.coordinator.data.current_weather_data.get("wind_bearing")
|
||||
|
||||
@property
|
||||
def native_wind_gust_speed(self) -> float | None:
|
||||
"""Return the wind gust speed in native units."""
|
||||
return self.coordinator.data.current_weather_data.get("wind_gust")
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float | None:
|
||||
"""Return the cloud coverage."""
|
||||
return self.coordinator.data.current_weather_data.get("cloudiness")
|
||||
|
||||
def _forecast(self, hourly: bool) -> list[Forecast]:
|
||||
"""Return the forecast array."""
|
||||
if hourly:
|
||||
|
||||
@@ -850,14 +850,6 @@ COFFEE_SYSTEM_PROGRAM_ID: dict[int, str] = {
|
||||
24813: "appliance_settings", # modify profile name
|
||||
}
|
||||
|
||||
COFFEE_SYSTEM_PROFILE: dict[range, str] = {
|
||||
range(24000, 24032): "profile_1",
|
||||
range(24032, 24064): "profile_2",
|
||||
range(24064, 24096): "profile_3",
|
||||
range(24096, 24128): "profile_4",
|
||||
range(24128, 24160): "profile_5",
|
||||
}
|
||||
|
||||
STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = {
|
||||
8: "steam_cooking",
|
||||
19: "microwave",
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Final, cast
|
||||
from typing import Final, cast
|
||||
|
||||
from pymiele import MieleDevice, MieleTemperature
|
||||
|
||||
@@ -30,7 +30,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import (
|
||||
COFFEE_SYSTEM_PROFILE,
|
||||
DISABLED_TEMP_ENTITIES,
|
||||
DOMAIN,
|
||||
STATE_PROGRAM_ID,
|
||||
@@ -62,8 +61,6 @@ PLATE_COUNT = {
|
||||
"KMX": 6,
|
||||
}
|
||||
|
||||
ATTRIBUTE_PROFILE = "profile"
|
||||
|
||||
|
||||
def _get_plate_count(tech_type: str) -> int:
|
||||
"""Get number of zones for hob."""
|
||||
@@ -91,21 +88,11 @@ def _convert_temperature(
|
||||
return raw_value
|
||||
|
||||
|
||||
def _get_coffee_profile(value: MieleDevice) -> str | None:
|
||||
"""Get coffee profile from value."""
|
||||
if value.state_program_id is not None:
|
||||
for key_range, profile in COFFEE_SYSTEM_PROFILE.items():
|
||||
if value.state_program_id in key_range:
|
||||
return profile
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MieleSensorDescription(SensorEntityDescription):
|
||||
"""Class describing Miele sensor entities."""
|
||||
|
||||
value_fn: Callable[[MieleDevice], StateType]
|
||||
extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None
|
||||
zone: int | None = None
|
||||
unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None
|
||||
|
||||
@@ -170,6 +157,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
MieleAppliance.OVEN_MICROWAVE,
|
||||
MieleAppliance.STEAM_OVEN,
|
||||
MieleAppliance.MICROWAVE,
|
||||
MieleAppliance.COFFEE_SYSTEM,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER,
|
||||
MieleAppliance.WASHER_DRYER,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
@@ -184,18 +172,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
value_fn=lambda value: value.state_program_id,
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(MieleAppliance.COFFEE_SYSTEM,),
|
||||
description=MieleSensorDescription(
|
||||
key="state_program_id",
|
||||
translation_key="program_id",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda value: value.state_program_id,
|
||||
extra_attributes={
|
||||
ATTRIBUTE_PROFILE: _get_coffee_profile,
|
||||
},
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(
|
||||
MieleAppliance.WASHING_MACHINE,
|
||||
@@ -734,16 +710,6 @@ class MieleSensor(MieleEntity, SensorEntity):
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.device)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return extra_state_attributes."""
|
||||
if self.entity_description.extra_attributes is None:
|
||||
return None
|
||||
attr = {}
|
||||
for key, value in self.entity_description.extra_attributes.items():
|
||||
attr[key] = value(self.device)
|
||||
return attr
|
||||
|
||||
|
||||
class MielePlateSensor(MieleSensor):
|
||||
"""Representation of a Sensor."""
|
||||
@@ -826,8 +792,6 @@ class MielePhaseSensor(MieleSensor):
|
||||
class MieleProgramIdSensor(MieleSensor):
|
||||
"""Representation of the program id sensor."""
|
||||
|
||||
_unrecorded_attributes = frozenset({ATTRIBUTE_PROFILE})
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -991,18 +991,6 @@
|
||||
"yom_tov": "Yom tov",
|
||||
"yorkshire_pudding": "Yorkshire pudding",
|
||||
"zander_fillet": "Zander (fillet)"
|
||||
},
|
||||
"state_attributes": {
|
||||
"profile": {
|
||||
"name": "Profile",
|
||||
"state": {
|
||||
"profile_1": "Profile 1",
|
||||
"profile_2": "Profile 2",
|
||||
"profile_3": "Profile 3",
|
||||
"profile_4": "Profile 4",
|
||||
"profile_5": "Profile 5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spin_speed": {
|
||||
|
||||
@@ -490,11 +490,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
||||
if hvac_mode == value:
|
||||
self._attr_hvac_mode = mode
|
||||
break
|
||||
else:
|
||||
# since there are no hvac_mode_register, this
|
||||
# integration should not touch the attr.
|
||||
# However it lacks in the climate component.
|
||||
self._attr_hvac_mode = HVACMode.AUTO
|
||||
|
||||
# Read the HVAC action register if defined
|
||||
if self._hvac_action_register is not None:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
import struct
|
||||
@@ -27,7 +28,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity, ToggleEntity
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import (
|
||||
@@ -91,6 +92,7 @@ class BasePlatform(Entity):
|
||||
self._scan_interval = int(entry[CONF_SCAN_INTERVAL])
|
||||
self._cancel_timer: Callable[[], None] | None = None
|
||||
self._cancel_call: Callable[[], None] | None = None
|
||||
|
||||
self._attr_unique_id = entry.get(CONF_UNIQUE_ID)
|
||||
self._attr_name = entry[CONF_NAME]
|
||||
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
|
||||
@@ -107,39 +109,29 @@ class BasePlatform(Entity):
|
||||
self._max_value = get_optional_numeric_config(CONF_MAX_VALUE)
|
||||
self._nan_value = entry.get(CONF_NAN_VALUE)
|
||||
self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS)
|
||||
self._update_lock = asyncio.Lock()
|
||||
|
||||
@abstractmethod
|
||||
async def _async_update(self) -> None:
|
||||
"""Virtual function to be overwritten."""
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def async_update(self, now: datetime | None = None) -> None:
|
||||
"""Update the entity state."""
|
||||
if self._cancel_call:
|
||||
self._cancel_call()
|
||||
await self.async_local_update()
|
||||
|
||||
async def async_local_update(self, now: datetime | None = None) -> None:
|
||||
"""Update the entity state."""
|
||||
await self._async_update()
|
||||
self.async_write_ha_state()
|
||||
if self._scan_interval > 0:
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass,
|
||||
timedelta(seconds=self._scan_interval),
|
||||
self.async_local_update,
|
||||
)
|
||||
async with self._update_lock:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update_write_state(self) -> None:
|
||||
"""Update the entity state and write it to the state machine."""
|
||||
if self._cancel_call:
|
||||
self._cancel_call()
|
||||
self._cancel_call = None
|
||||
await self.async_local_update()
|
||||
await self.async_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_update_if_not_in_progress(
|
||||
self, now: datetime | None = None
|
||||
) -> None:
|
||||
"""Update the entity state if not already in progress."""
|
||||
if self._update_lock.locked():
|
||||
_LOGGER.debug("Update for entity %s is already in progress", self.name)
|
||||
return
|
||||
await self._async_update_write_state()
|
||||
|
||||
@callback
|
||||
@@ -147,9 +139,12 @@ class BasePlatform(Entity):
|
||||
"""Remote start entity."""
|
||||
self._async_cancel_update_polling()
|
||||
self._async_schedule_future_update(0.1)
|
||||
self._cancel_call = async_call_later(
|
||||
self.hass, timedelta(seconds=0.1), self.async_local_update
|
||||
)
|
||||
if self._scan_interval > 0:
|
||||
self._cancel_timer = async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_update_if_not_in_progress,
|
||||
timedelta(seconds=self._scan_interval),
|
||||
)
|
||||
self._attr_available = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -182,20 +177,9 @@ class BasePlatform(Entity):
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_await_connection(self, _now: Any) -> None:
|
||||
"""Wait for first connect."""
|
||||
await self._hub.event_connected.wait()
|
||||
self.async_run()
|
||||
|
||||
async def async_base_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
self.async_on_remove(
|
||||
async_call_later(
|
||||
self.hass,
|
||||
self._hub.config_delay + 0.1,
|
||||
self.async_await_connection,
|
||||
)
|
||||
)
|
||||
self.async_run()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_hold)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from pymodbus.client import (
|
||||
@@ -27,10 +28,11 @@ from homeassistant.const import (
|
||||
CONF_TYPE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -71,7 +73,6 @@ from .validators import check_config
|
||||
|
||||
DATA_MODBUS_HUBS: HassKey[dict[str, ModbusHub]] = HassKey(DOMAIN)
|
||||
|
||||
PRIMARY_RECONNECT_DELAY = 60
|
||||
|
||||
ConfEntry = namedtuple("ConfEntry", "call_type attr func_name value_attr_name") # noqa: PYI024
|
||||
RunEntry = namedtuple("RunEntry", "attr func value_attr_name") # noqa: PYI024
|
||||
@@ -253,12 +254,13 @@ class ModbusHub:
|
||||
self._client: (
|
||||
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
|
||||
) = None
|
||||
self._async_cancel_listener: Callable[[], None] | None = None
|
||||
self._in_error = False
|
||||
self._lock = asyncio.Lock()
|
||||
self.event_connected = asyncio.Event()
|
||||
self.hass = hass
|
||||
self.name = client_config[CONF_NAME]
|
||||
self._config_type = client_config[CONF_TYPE]
|
||||
self.config_delay = client_config[CONF_DELAY]
|
||||
self._config_delay = client_config[CONF_DELAY]
|
||||
self._pb_request: dict[str, RunEntry] = {}
|
||||
self._connect_task: asyncio.Task
|
||||
self._last_log_error: str = ""
|
||||
@@ -311,25 +313,22 @@ class ModbusHub:
|
||||
|
||||
async def async_pb_connect(self) -> None:
|
||||
"""Connect to device, async."""
|
||||
while True:
|
||||
async with self._lock:
|
||||
try:
|
||||
if await self._client.connect(): # type: ignore[union-attr]
|
||||
_LOGGER.info(f"modbus {self.name} communication open")
|
||||
break
|
||||
except ModbusException as exception_error:
|
||||
self._log_error(
|
||||
f"{self.name} connect failed, please check your configuration ({exception_error!s})"
|
||||
)
|
||||
_LOGGER.info(
|
||||
f"modbus {self.name} connect NOT a success ! retrying in {PRIMARY_RECONNECT_DELAY} seconds"
|
||||
)
|
||||
await asyncio.sleep(PRIMARY_RECONNECT_DELAY)
|
||||
async with self._lock:
|
||||
try:
|
||||
await self._client.connect() # type: ignore[union-attr]
|
||||
except ModbusException as exception_error:
|
||||
self._log_error(
|
||||
f"{self.name} connect failed, please check your configuration ({exception_error!s})"
|
||||
)
|
||||
return
|
||||
message = f"modbus {self.name} communication open"
|
||||
_LOGGER.info(message)
|
||||
|
||||
if self.config_delay:
|
||||
await asyncio.sleep(self.config_delay)
|
||||
self.config_delay = 0
|
||||
self.event_connected.set()
|
||||
# Start counting down to allow modbus requests.
|
||||
if self._config_delay:
|
||||
self._async_cancel_listener = async_call_later(
|
||||
self.hass, self._config_delay, self.async_end_delay
|
||||
)
|
||||
|
||||
async def async_setup(self) -> bool:
|
||||
"""Set up pymodbus client."""
|
||||
@@ -350,6 +349,12 @@ class ModbusHub:
|
||||
)
|
||||
return True
|
||||
|
||||
@callback
|
||||
def async_end_delay(self, args: Any) -> None:
|
||||
"""End startup delay."""
|
||||
self._async_cancel_listener = None
|
||||
self._config_delay = 0
|
||||
|
||||
async def async_restart(self) -> None:
|
||||
"""Reconnect client."""
|
||||
if self._client:
|
||||
@@ -359,6 +364,9 @@ class ModbusHub:
|
||||
|
||||
async def async_close(self) -> None:
|
||||
"""Disconnect client."""
|
||||
if self._async_cancel_listener:
|
||||
self._async_cancel_listener()
|
||||
self._async_cancel_listener = None
|
||||
if not self._connect_task.done():
|
||||
self._connect_task.cancel()
|
||||
|
||||
@@ -407,6 +415,7 @@ class ModbusHub:
|
||||
error = f"Error: device: {slave} address: {address} -> pymodbus returned isError True"
|
||||
self._log_error(error)
|
||||
return None
|
||||
self._in_error = False
|
||||
return result
|
||||
|
||||
async def async_pb_call(
|
||||
@@ -417,6 +426,8 @@ class ModbusHub:
|
||||
use_call: str,
|
||||
) -> ModbusPDU | None:
|
||||
"""Convert async to sync pymodbus call."""
|
||||
if self._config_delay:
|
||||
return None
|
||||
async with self._lock:
|
||||
if not self._client:
|
||||
return None
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.components.number import (
|
||||
NumberMode,
|
||||
RestoreNumber,
|
||||
)
|
||||
from homeassistant.components.sensor import AMBIGUOUS_UNITS
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -71,12 +70,6 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the configuration is valid, throws if it isn't."""
|
||||
if (
|
||||
CONF_UNIT_OF_MEASUREMENT in config
|
||||
and (unit_of_measurement := config[CONF_UNIT_OF_MEASUREMENT]) in AMBIGUOUS_UNITS
|
||||
):
|
||||
config[CONF_UNIT_OF_MEASUREMENT] = AMBIGUOUS_UNITS[unit_of_measurement]
|
||||
|
||||
if config[CONF_MIN] > config[CONF_MAX]:
|
||||
raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}")
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import sensor
|
||||
from homeassistant.components.sensor import (
|
||||
AMBIGUOUS_UNITS,
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASS_UNITS,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
@@ -134,14 +133,9 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
f"together with state class '{state_class}'"
|
||||
)
|
||||
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None:
|
||||
return config
|
||||
|
||||
unit_of_measurement = config[CONF_UNIT_OF_MEASUREMENT] = AMBIGUOUS_UNITS.get(
|
||||
unit_of_measurement, unit_of_measurement
|
||||
)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None:
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
|
||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
) is None:
|
||||
return config
|
||||
|
||||
if (
|
||||
|
||||
@@ -10,12 +10,7 @@ from typing import Any, Final, cast
|
||||
from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.json import save_json
|
||||
@@ -205,9 +200,7 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="unknown")
|
||||
name = self.nanoleaf.name
|
||||
|
||||
await self.async_set_unique_id(
|
||||
name, raise_on_progress=self.source != SOURCE_USER
|
||||
)
|
||||
await self.async_set_unique_id(name)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host})
|
||||
|
||||
if discovery_integration_import:
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==9.2.3"]
|
||||
"requirements": ["pyatmo==9.2.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aionfty"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiontfy==0.5.4"]
|
||||
"requirements": ["aiontfy==0.5.3"]
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ from homeassistant.loader import async_suggest_report_issue
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
AMBIGUOUS_UNITS,
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
@@ -369,15 +368,6 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
return None
|
||||
|
||||
@final
|
||||
@property
|
||||
def __native_unit_of_measurement_compat(self) -> str | None:
|
||||
"""Process ambiguous units."""
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
return AMBIGUOUS_UNITS.get(
|
||||
native_unit_of_measurement, native_unit_of_measurement
|
||||
)
|
||||
|
||||
@property
|
||||
@final
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
@@ -385,7 +375,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if self._number_option_unit_of_measurement:
|
||||
return self._number_option_unit_of_measurement
|
||||
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
# device_class is checked after native_unit_of_measurement since most
|
||||
# of the time we can avoid the device_class check
|
||||
if (
|
||||
@@ -454,7 +444,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if device_class not in UNIT_CONVERTERS:
|
||||
return value
|
||||
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
unit_of_measurement = self.unit_of_measurement
|
||||
if native_unit_of_measurement != unit_of_measurement:
|
||||
if TYPE_CHECKING:
|
||||
@@ -483,7 +473,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS:
|
||||
return value
|
||||
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
unit_of_measurement = self.unit_of_measurement
|
||||
if native_unit_of_measurement != unit_of_measurement:
|
||||
if TYPE_CHECKING:
|
||||
@@ -506,7 +496,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
(number_options := self.registry_entry.options.get(DOMAIN))
|
||||
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||
and self.__native_unit_of_measurement_compat
|
||||
and self.native_unit_of_measurement
|
||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
):
|
||||
|
||||
@@ -9,7 +9,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
@@ -137,7 +136,7 @@ class NumberDeviceClass(StrEnum):
|
||||
CONDUCTIVITY = "conductivity"
|
||||
"""Conductivity.
|
||||
|
||||
Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
|
||||
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm`
|
||||
"""
|
||||
|
||||
CURRENT = "current"
|
||||
@@ -169,7 +168,7 @@ class NumberDeviceClass(StrEnum):
|
||||
DURATION = "duration"
|
||||
"""Fixed duration.
|
||||
|
||||
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
|
||||
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs`
|
||||
"""
|
||||
|
||||
ENERGY = "energy"
|
||||
@@ -247,25 +246,25 @@ class NumberDeviceClass(StrEnum):
|
||||
NITROGEN_DIOXIDE = "nitrogen_dioxide"
|
||||
"""Amount of NO2.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
NITROGEN_MONOXIDE = "nitrogen_monoxide"
|
||||
"""Amount of NO.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
NITROUS_OXIDE = "nitrous_oxide"
|
||||
"""Amount of N2O.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
OZONE = "ozone"
|
||||
"""Amount of O3.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PH = "ph"
|
||||
@@ -277,19 +276,19 @@ class NumberDeviceClass(StrEnum):
|
||||
PM1 = "pm1"
|
||||
"""Particulate matter <= 1 μm.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM10 = "pm10"
|
||||
"""Particulate matter <= 10 μm.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM25 = "pm25"
|
||||
"""Particulate matter <= 2.5 μm.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
POWER_FACTOR = "power_factor"
|
||||
@@ -366,7 +365,7 @@ class NumberDeviceClass(StrEnum):
|
||||
SULPHUR_DIOXIDE = "sulphur_dioxide"
|
||||
"""Amount of SO2.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
TEMPERATURE = "temperature"
|
||||
@@ -378,7 +377,7 @@ class NumberDeviceClass(StrEnum):
|
||||
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
|
||||
"""Amount of VOC.
|
||||
|
||||
Unit of measurement: `μg/m³`, `mg/m³`
|
||||
Unit of measurement: `µg/m³`, `mg/m³`
|
||||
"""
|
||||
|
||||
VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
|
||||
@@ -390,7 +389,7 @@ class NumberDeviceClass(StrEnum):
|
||||
VOLTAGE = "voltage"
|
||||
"""Voltage.
|
||||
|
||||
Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
|
||||
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV`
|
||||
"""
|
||||
|
||||
VOLUME = "volume"
|
||||
@@ -437,7 +436,7 @@ class NumberDeviceClass(StrEnum):
|
||||
Weight is used instead of mass to fit with every day language.
|
||||
|
||||
Unit of measurement: `MASS_*` units
|
||||
- SI / metric: `μg`, `mg`, `g`, `kg`
|
||||
- SI / metric: `µg`, `mg`, `g`, `kg`
|
||||
- USCS / imperial: `oz`, `lb`
|
||||
"""
|
||||
|
||||
@@ -557,16 +556,3 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
|
||||
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
|
||||
NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter,
|
||||
}
|
||||
|
||||
# We translate units that were using using the legacy coding of μ \u00b5
|
||||
# to units using recommended coding of μ \u03bc
|
||||
AMBIGUOUS_UNITS: dict[str | None, str] = {
|
||||
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
|
||||
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
|
||||
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
|
||||
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
|
||||
"\u00b5g": UnitOfMass.MICROGRAMS,
|
||||
"\u00b5s": UnitOfTime.MICROSECONDS,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ from openai._streaming import AsyncStream
|
||||
from openai.types.responses import (
|
||||
EasyInputMessageParam,
|
||||
FunctionToolParam,
|
||||
ResponseCodeInterpreterToolCall,
|
||||
ResponseCompletedEvent,
|
||||
ResponseErrorEvent,
|
||||
ResponseFailedEvent,
|
||||
@@ -22,8 +21,6 @@ from openai.types.responses import (
|
||||
ResponseFunctionCallArgumentsDoneEvent,
|
||||
ResponseFunctionToolCall,
|
||||
ResponseFunctionToolCallParam,
|
||||
ResponseFunctionWebSearch,
|
||||
ResponseFunctionWebSearchParam,
|
||||
ResponseIncompleteEvent,
|
||||
ResponseInputFileParam,
|
||||
ResponseInputImageParam,
|
||||
@@ -95,8 +92,6 @@ MAX_TOOL_ITERATIONS = 10
|
||||
def _adjust_schema(schema: dict[str, Any]) -> None:
|
||||
"""Adjust the schema to be compatible with OpenAI API."""
|
||||
if schema["type"] == "object":
|
||||
schema.setdefault("strict", True)
|
||||
schema.setdefault("additionalProperties", False)
|
||||
if "properties" not in schema:
|
||||
return
|
||||
|
||||
@@ -130,6 +125,8 @@ def _format_structured_output(
|
||||
|
||||
_adjust_schema(result)
|
||||
|
||||
result["strict"] = True
|
||||
result["additionalProperties"] = False
|
||||
return result
|
||||
|
||||
|
||||
@@ -152,27 +149,16 @@ def _convert_content_to_param(
|
||||
"""Convert any native chat message for this agent to the native format."""
|
||||
messages: ResponseInputParam = []
|
||||
reasoning_summary: list[str] = []
|
||||
web_search_calls: dict[str, ResponseFunctionWebSearchParam] = {}
|
||||
|
||||
for content in chat_content:
|
||||
if isinstance(content, conversation.ToolResultContent):
|
||||
if (
|
||||
content.tool_name == "web_search_call"
|
||||
and content.tool_call_id in web_search_calls
|
||||
):
|
||||
web_search_call = web_search_calls.pop(content.tool_call_id)
|
||||
web_search_call["status"] = content.tool_result.get( # type: ignore[typeddict-item]
|
||||
"status", "completed"
|
||||
)
|
||||
messages.append(web_search_call)
|
||||
else:
|
||||
messages.append(
|
||||
FunctionCallOutput(
|
||||
type="function_call_output",
|
||||
call_id=content.tool_call_id,
|
||||
output=json.dumps(content.tool_result),
|
||||
)
|
||||
messages.append(
|
||||
FunctionCallOutput(
|
||||
type="function_call_output",
|
||||
call_id=content.tool_call_id,
|
||||
output=json.dumps(content.tool_result),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if content.content:
|
||||
@@ -187,27 +173,15 @@ def _convert_content_to_param(
|
||||
|
||||
if isinstance(content, conversation.AssistantContent):
|
||||
if content.tool_calls:
|
||||
for tool_call in content.tool_calls:
|
||||
if (
|
||||
tool_call.external
|
||||
and tool_call.tool_name == "web_search_call"
|
||||
and "action" in tool_call.tool_args
|
||||
):
|
||||
web_search_calls[tool_call.id] = ResponseFunctionWebSearchParam(
|
||||
type="web_search_call",
|
||||
id=tool_call.id,
|
||||
action=tool_call.tool_args["action"],
|
||||
status="completed",
|
||||
)
|
||||
else:
|
||||
messages.append(
|
||||
ResponseFunctionToolCallParam(
|
||||
type="function_call",
|
||||
name=tool_call.tool_name,
|
||||
arguments=json.dumps(tool_call.tool_args),
|
||||
call_id=tool_call.id,
|
||||
)
|
||||
)
|
||||
messages.extend(
|
||||
ResponseFunctionToolCallParam(
|
||||
type="function_call",
|
||||
name=tool_call.tool_name,
|
||||
arguments=json.dumps(tool_call.tool_args),
|
||||
call_id=tool_call.id,
|
||||
)
|
||||
for tool_call in content.tool_calls
|
||||
)
|
||||
|
||||
if content.thinking_content:
|
||||
reasoning_summary.append(content.thinking_content)
|
||||
@@ -237,37 +211,25 @@ def _convert_content_to_param(
|
||||
async def _transform_stream(
|
||||
chat_log: conversation.ChatLog,
|
||||
stream: AsyncStream[ResponseStreamEvent],
|
||||
) -> AsyncGenerator[
|
||||
conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict
|
||||
]:
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
"""Transform an OpenAI delta stream into HA format."""
|
||||
last_summary_index = None
|
||||
last_role: Literal["assistant", "tool_result"] | None = None
|
||||
|
||||
async for event in stream:
|
||||
LOGGER.debug("Received event: %s", event)
|
||||
|
||||
if isinstance(event, ResponseOutputItemAddedEvent):
|
||||
if isinstance(event.item, ResponseFunctionToolCall):
|
||||
if isinstance(event.item, ResponseOutputMessage):
|
||||
yield {"role": event.item.role}
|
||||
last_summary_index = None
|
||||
elif isinstance(event.item, ResponseFunctionToolCall):
|
||||
# OpenAI has tool calls as individual events
|
||||
# while HA puts tool calls inside the assistant message.
|
||||
# We turn them into individual assistant content for HA
|
||||
# to ensure that tools are called as soon as possible.
|
||||
yield {"role": "assistant"}
|
||||
last_role = "assistant"
|
||||
last_summary_index = None
|
||||
current_tool_call = event.item
|
||||
elif (
|
||||
isinstance(event.item, ResponseOutputMessage)
|
||||
or (
|
||||
isinstance(event.item, ResponseReasoningItem)
|
||||
and last_summary_index is not None
|
||||
) # Subsequent ResponseReasoningItem
|
||||
or last_role != "assistant"
|
||||
):
|
||||
yield {"role": "assistant"}
|
||||
last_role = "assistant"
|
||||
last_summary_index = None
|
||||
elif isinstance(event, ResponseOutputItemDoneEvent):
|
||||
if isinstance(event.item, ResponseReasoningItem):
|
||||
yield {
|
||||
@@ -278,52 +240,6 @@ async def _transform_stream(
|
||||
encrypted_content=event.item.encrypted_content,
|
||||
)
|
||||
}
|
||||
last_summary_index = len(event.item.summary) - 1
|
||||
elif isinstance(event.item, ResponseCodeInterpreterToolCall):
|
||||
yield {
|
||||
"tool_calls": [
|
||||
llm.ToolInput(
|
||||
id=event.item.id,
|
||||
tool_name="code_interpreter",
|
||||
tool_args={
|
||||
"code": event.item.code,
|
||||
"container": event.item.container_id,
|
||||
},
|
||||
external=True,
|
||||
)
|
||||
]
|
||||
}
|
||||
yield {
|
||||
"role": "tool_result",
|
||||
"tool_call_id": event.item.id,
|
||||
"tool_name": "code_interpreter",
|
||||
"tool_result": {
|
||||
"output": [output.to_dict() for output in event.item.outputs] # type: ignore[misc]
|
||||
if event.item.outputs is not None
|
||||
else None
|
||||
},
|
||||
}
|
||||
last_role = "tool_result"
|
||||
elif isinstance(event.item, ResponseFunctionWebSearch):
|
||||
yield {
|
||||
"tool_calls": [
|
||||
llm.ToolInput(
|
||||
id=event.item.id,
|
||||
tool_name="web_search_call",
|
||||
tool_args={
|
||||
"action": event.item.action.to_dict(),
|
||||
},
|
||||
external=True,
|
||||
)
|
||||
]
|
||||
}
|
||||
yield {
|
||||
"role": "tool_result",
|
||||
"tool_call_id": event.item.id,
|
||||
"tool_name": "web_search_call",
|
||||
"tool_result": {"status": event.item.status},
|
||||
}
|
||||
last_role = "tool_result"
|
||||
elif isinstance(event, ResponseTextDeltaEvent):
|
||||
yield {"content": event.delta}
|
||||
elif isinstance(event, ResponseReasoningSummaryTextDeltaEvent):
|
||||
@@ -336,7 +252,6 @@ async def _transform_stream(
|
||||
and event.summary_index != last_summary_index
|
||||
):
|
||||
yield {"role": "assistant"}
|
||||
last_role = "assistant"
|
||||
last_summary_index = event.summary_index
|
||||
yield {"thinking_content": event.delta}
|
||||
elif isinstance(event, ResponseFunctionCallArgumentsDeltaEvent):
|
||||
@@ -433,33 +348,6 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
"""Generate an answer for the chat log."""
|
||||
options = self.subentry.data
|
||||
|
||||
messages = _convert_content_to_param(chat_log.content)
|
||||
|
||||
model_args = ResponseCreateParamsStreaming(
|
||||
model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
input=messages,
|
||||
max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
top_p=options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
||||
user=chat_log.conversation_id,
|
||||
store=False,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
if model_args["model"].startswith(("o", "gpt-5")):
|
||||
model_args["reasoning"] = {
|
||||
"effort": options.get(
|
||||
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
|
||||
),
|
||||
"summary": "auto",
|
||||
}
|
||||
model_args["include"] = ["reasoning.encrypted_content"]
|
||||
|
||||
if model_args["model"].startswith("gpt-5"):
|
||||
model_args["text"] = {
|
||||
"verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY)
|
||||
}
|
||||
|
||||
tools: list[ToolParam] = []
|
||||
if chat_log.llm_api:
|
||||
tools = [
|
||||
@@ -493,11 +381,36 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
),
|
||||
)
|
||||
)
|
||||
model_args.setdefault("include", []).append("code_interpreter_call.outputs") # type: ignore[union-attr]
|
||||
|
||||
messages = _convert_content_to_param(chat_log.content)
|
||||
|
||||
model_args = ResponseCreateParamsStreaming(
|
||||
model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
input=messages,
|
||||
max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
top_p=options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
||||
user=chat_log.conversation_id,
|
||||
store=False,
|
||||
stream=True,
|
||||
)
|
||||
if tools:
|
||||
model_args["tools"] = tools
|
||||
|
||||
if model_args["model"].startswith(("o", "gpt-5")):
|
||||
model_args["reasoning"] = {
|
||||
"effort": options.get(
|
||||
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
|
||||
),
|
||||
"summary": "auto",
|
||||
}
|
||||
model_args["include"] = ["reasoning.encrypted_content"]
|
||||
|
||||
if model_args["model"].startswith("gpt-5"):
|
||||
model_args["text"] = {
|
||||
"verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY)
|
||||
}
|
||||
|
||||
last_content = chat_log.content[-1]
|
||||
|
||||
# Handle attachments by adding them to the last user message
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.15.2"]
|
||||
"requirements": ["opower==0.15.1"]
|
||||
}
|
||||
|
||||
@@ -366,7 +366,6 @@ class PrometheusMetrics:
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_metric_name(metric: str) -> str:
|
||||
metric.replace("\u03bc", "\u00b5")
|
||||
return "".join(
|
||||
[c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric]
|
||||
)
|
||||
@@ -748,9 +747,6 @@ class PrometheusMetrics:
|
||||
PERCENTAGE: "percent",
|
||||
}
|
||||
default = unit.replace("/", "_per_")
|
||||
# Unit conversion for CONCENTRATION_MICROGRAMS_PER_CUBIC_METER "μg/m³"
|
||||
# "μ" == "\u03bc" but the API uses "\u00b5"
|
||||
default = default.replace("\u03bc", "\u00b5")
|
||||
default = default.lower()
|
||||
return units.get(unit, default)
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ def correct_db_schema_precision(
|
||||
from ..migration import _modify_columns # noqa: PLC0415
|
||||
|
||||
precision_columns = _get_precision_column_types(table_object)
|
||||
# Attempt to convert timestamp columns to μs precision
|
||||
# Attempt to convert timestamp columns to µs precision
|
||||
session_maker = instance.get_session
|
||||
engine = instance.engine
|
||||
assert engine is not None, "Engine should be set"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.4.0"]
|
||||
"requirements": ["renault-api==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -156,7 +156,6 @@ class RenaultHub:
|
||||
name=vehicle.device_info[ATTR_NAME],
|
||||
model=vehicle.device_info[ATTR_MODEL],
|
||||
model_id=vehicle.device_info[ATTR_MODEL_ID],
|
||||
sw_version=None, # cleanup from PR #125399
|
||||
)
|
||||
self._vehicles[vehicle_link.vin] = vehicle
|
||||
|
||||
|
||||
@@ -137,9 +137,9 @@ class RepairsFlowIndexView(FlowManagerIndexView):
|
||||
"Handler does not support user", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
return self.json(
|
||||
self._prepare_result_json(result),
|
||||
)
|
||||
result = self._prepare_result_json(result)
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class RepairsFlowResourceView(FlowManagerResourceView):
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==4.8.1"],
|
||||
"requirements": ["aiorussound==4.8.0"],
|
||||
"zeroconf": ["_rio._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ from homeassistant.util.enum import try_parse_enum
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
AMBIGUOUS_UNITS,
|
||||
ATTR_LAST_RESET,
|
||||
ATTR_OPTIONS,
|
||||
ATTR_STATE_CLASS,
|
||||
@@ -315,7 +314,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return _numeric_state_expected(
|
||||
try_parse_enum(SensorDeviceClass, self.device_class),
|
||||
self.state_class,
|
||||
self.__native_unit_of_measurement_compat,
|
||||
self.native_unit_of_measurement,
|
||||
self.suggested_display_precision,
|
||||
)
|
||||
|
||||
@@ -367,8 +366,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
# Make sure we can convert the units
|
||||
if (
|
||||
(unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None
|
||||
or self.__native_unit_of_measurement_compat
|
||||
not in unit_converter.VALID_UNITS
|
||||
or self.native_unit_of_measurement not in unit_converter.VALID_UNITS
|
||||
or suggested_unit_of_measurement not in unit_converter.VALID_UNITS
|
||||
):
|
||||
if not self._invalid_suggested_unit_of_measurement_reported:
|
||||
@@ -389,7 +387,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if suggested_unit_of_measurement is None:
|
||||
# Fallback to unit suggested by the unit conversion rules from device class
|
||||
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
|
||||
self.device_class, self.__native_unit_of_measurement_compat
|
||||
self.device_class, self.native_unit_of_measurement
|
||||
)
|
||||
|
||||
if suggested_unit_of_measurement is None and (
|
||||
@@ -398,7 +396,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
# If the device class is not known by the unit system but has a unit converter,
|
||||
# fall back to the unit suggested by the unit converter's unit class.
|
||||
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
|
||||
unit_converter.UNIT_CLASS, self.__native_unit_of_measurement_compat
|
||||
unit_converter.UNIT_CLASS, self.native_unit_of_measurement
|
||||
)
|
||||
|
||||
if suggested_unit_of_measurement is None:
|
||||
@@ -470,16 +468,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
return None
|
||||
|
||||
@final
|
||||
@property
|
||||
def __native_unit_of_measurement_compat(self) -> str | None:
|
||||
"""Process ambiguous units."""
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
return AMBIGUOUS_UNITS.get(
|
||||
native_unit_of_measurement,
|
||||
native_unit_of_measurement,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def suggested_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit which should be used for the sensor's state.
|
||||
@@ -515,7 +503,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if self._sensor_option_unit_of_measurement is not UNDEFINED:
|
||||
return self._sensor_option_unit_of_measurement
|
||||
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
|
||||
# Second priority, for non registered entities: unit suggested by integration
|
||||
if not self.registry_entry and (
|
||||
@@ -555,7 +543,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
@override
|
||||
def state(self) -> Any:
|
||||
"""Return the state of the sensor and perform unit conversions, if needed."""
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
unit_of_measurement = self.unit_of_measurement
|
||||
value = self.native_value
|
||||
# For the sake of validation, we can ignore custom device classes
|
||||
@@ -777,8 +765,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return display_precision
|
||||
|
||||
default_unit_of_measurement = (
|
||||
self.suggested_unit_of_measurement
|
||||
or self.__native_unit_of_measurement_compat
|
||||
self.suggested_unit_of_measurement or self.native_unit_of_measurement
|
||||
)
|
||||
if default_unit_of_measurement is None:
|
||||
return display_precision
|
||||
@@ -856,7 +843,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
(sensor_options := self.registry_entry.options.get(primary_key))
|
||||
and secondary_key in sensor_options
|
||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||
and self.__native_unit_of_measurement_compat
|
||||
and self.native_unit_of_measurement
|
||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
and (custom_unit := sensor_options[secondary_key])
|
||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
|
||||
@@ -9,7 +9,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
@@ -168,7 +167,7 @@ class SensorDeviceClass(StrEnum):
|
||||
CONDUCTIVITY = "conductivity"
|
||||
"""Conductivity.
|
||||
|
||||
Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
|
||||
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm`
|
||||
"""
|
||||
|
||||
CURRENT = "current"
|
||||
@@ -200,7 +199,7 @@ class SensorDeviceClass(StrEnum):
|
||||
DURATION = "duration"
|
||||
"""Fixed duration.
|
||||
|
||||
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
|
||||
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs`
|
||||
"""
|
||||
|
||||
ENERGY = "energy"
|
||||
@@ -280,25 +279,25 @@ class SensorDeviceClass(StrEnum):
|
||||
NITROGEN_DIOXIDE = "nitrogen_dioxide"
|
||||
"""Amount of NO2.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
NITROGEN_MONOXIDE = "nitrogen_monoxide"
|
||||
"""Amount of NO.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
NITROUS_OXIDE = "nitrous_oxide"
|
||||
"""Amount of N2O.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
OZONE = "ozone"
|
||||
"""Amount of O3.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PH = "ph"
|
||||
@@ -310,19 +309,19 @@ class SensorDeviceClass(StrEnum):
|
||||
PM1 = "pm1"
|
||||
"""Particulate matter <= 1 μm.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM10 = "pm10"
|
||||
"""Particulate matter <= 10 μm.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM25 = "pm25"
|
||||
"""Particulate matter <= 2.5 μm.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
POWER_FACTOR = "power_factor"
|
||||
@@ -400,7 +399,7 @@ class SensorDeviceClass(StrEnum):
|
||||
SULPHUR_DIOXIDE = "sulphur_dioxide"
|
||||
"""Amount of SO2.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
TEMPERATURE = "temperature"
|
||||
@@ -412,7 +411,7 @@ class SensorDeviceClass(StrEnum):
|
||||
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
|
||||
"""Amount of VOC.
|
||||
|
||||
Unit of measurement: `μg/m³`, `mg/m³`
|
||||
Unit of measurement: `µg/m³`, `mg/m³`
|
||||
"""
|
||||
|
||||
VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
|
||||
@@ -424,7 +423,7 @@ class SensorDeviceClass(StrEnum):
|
||||
VOLTAGE = "voltage"
|
||||
"""Voltage.
|
||||
|
||||
Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
|
||||
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV`
|
||||
"""
|
||||
|
||||
VOLUME = "volume"
|
||||
@@ -471,7 +470,7 @@ class SensorDeviceClass(StrEnum):
|
||||
Weight is used instead of mass to fit with every day language.
|
||||
|
||||
Unit of measurement: `MASS_*` units
|
||||
- SI / metric: `μg`, `mg`, `g`, `kg`
|
||||
- SI / metric: `µg`, `mg`, `g`, `kg`
|
||||
- USCS / imperial: `oz`, `lb`
|
||||
"""
|
||||
|
||||
@@ -789,16 +788,3 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
|
||||
STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] = {
|
||||
SensorStateClass.MEASUREMENT_ANGLE: {DEGREE},
|
||||
}
|
||||
|
||||
# We translate units that were using using the legacy coding of μ \u00b5
|
||||
# to units using recommended coding of μ \u03bc
|
||||
AMBIGUOUS_UNITS: dict[str | None, str] = {
|
||||
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
|
||||
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
|
||||
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
|
||||
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
|
||||
"\u00b5g": UnitOfMass.MICROGRAMS,
|
||||
"\u00b5s": UnitOfTime.MICROSECONDS,
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ from homeassistant.util.enum import try_parse_enum
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
AMBIGUOUS_UNITS,
|
||||
ATTR_LAST_RESET,
|
||||
ATTR_STATE_CLASS,
|
||||
DOMAIN,
|
||||
@@ -80,7 +79,7 @@ EQUIVALENT_UNITS = {
|
||||
"ft3": UnitOfVolume.CUBIC_FEET,
|
||||
"m3": UnitOfVolume.CUBIC_METERS,
|
||||
"ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE,
|
||||
} | AMBIGUOUS_UNITS
|
||||
}
|
||||
|
||||
|
||||
# Keep track of entities for which a warning about decreasing value has been logged
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sleepiq",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["asyncsleepiq"],
|
||||
"requirements": ["asyncsleepiq==1.6.0"]
|
||||
"requirements": ["asyncsleepiq==1.5.3"]
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.2.9"]
|
||||
"requirements": ["pysmartthings==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
},
|
||||
"select": {
|
||||
"speech_dialog_level": {
|
||||
"name": "Speech enhancement",
|
||||
"name": "Dialog level",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["spotifyaio"],
|
||||
"requirements": ["spotifyaio==1.0.0"]
|
||||
"requirements": ["spotifyaio==0.8.11"]
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==0.68.4"]
|
||||
"requirements": ["PySwitchbot==0.68.3"]
|
||||
}
|
||||
|
||||
@@ -184,11 +184,6 @@ async def make_device_data(
|
||||
devices_data.buttons.append((device, coordinator))
|
||||
else:
|
||||
devices_data.switches.append((device, coordinator))
|
||||
if isinstance(device, Device) and device.device_type.startswith("Air Purifier"):
|
||||
coordinator = await coordinator_for_device(
|
||||
hass, entry, api, device, coordinators_by_id
|
||||
)
|
||||
devices_data.fans.append((device, coordinator))
|
||||
|
||||
if isinstance(device, Device) and device.device_type in [
|
||||
"Battery Circulator Fan",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Constants for the SwitchBot Cloud integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "switchbot_cloud"
|
||||
@@ -18,18 +17,5 @@ VACUUM_FAN_SPEED_STRONG = "strong"
|
||||
VACUUM_FAN_SPEED_MAX = "max"
|
||||
|
||||
AFTER_COMMAND_REFRESH = 5
|
||||
|
||||
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
|
||||
|
||||
|
||||
class AirPurifierMode(Enum):
|
||||
"""Air Purifier Modes."""
|
||||
|
||||
NORMAL = 1
|
||||
AUTO = 2
|
||||
SLEEP = 3
|
||||
PET = 4
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> list[str]:
|
||||
"""Return a list of available air purifier modes as lowercase strings."""
|
||||
return [mode.name.lower() for mode in cls]
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
"""Support for the Switchbot Battery Circulator fan."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import (
|
||||
AirPurifierCommands,
|
||||
BatteryCirculatorFanCommands,
|
||||
BatteryCirculatorFanMode,
|
||||
CommonCommands,
|
||||
SwitchBotAPI,
|
||||
)
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN, AirPurifierMode
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -33,13 +26,10 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
for device, coordinator in data.devices.fans:
|
||||
if device.device_type.startswith("Air Purifier"):
|
||||
async_add_entities(
|
||||
[SwitchBotAirPurifierEntity(data.api, device, coordinator)]
|
||||
)
|
||||
else:
|
||||
async_add_entities([SwitchBotCloudFan(data.api, device, coordinator)])
|
||||
async_add_entities(
|
||||
SwitchBotCloudFan(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.fans
|
||||
)
|
||||
|
||||
|
||||
class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity):
|
||||
@@ -47,7 +37,6 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity):
|
||||
|
||||
_attr_name = None
|
||||
|
||||
_api: SwitchBotAPI
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.SET_SPEED
|
||||
| FanEntityFeature.PRESET_MODE
|
||||
@@ -129,75 +118,3 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity):
|
||||
)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
class SwitchBotAirPurifierEntity(SwitchBotCloudEntity, FanEntity):
|
||||
"""Representation of a Switchbot air purifier."""
|
||||
|
||||
_api: SwitchBotAPI
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.PRESET_MODE
|
||||
| FanEntityFeature.TURN_OFF
|
||||
| FanEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_preset_modes = AirPurifierMode.get_modes()
|
||||
_attr_translation_key = "air_purifier"
|
||||
_attr_name = None
|
||||
_attr_is_on: bool | None = None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if device is on."""
|
||||
return self._attr_is_on
|
||||
|
||||
def _set_attributes(self) -> None:
|
||||
"""Set attributes from coordinator data."""
|
||||
if self.coordinator.data is None:
|
||||
return
|
||||
|
||||
self._attr_is_on = self.coordinator.data.get("power") == STATE_ON.upper()
|
||||
mode = self.coordinator.data.get("mode")
|
||||
self._attr_preset_mode = (
|
||||
AirPurifierMode(mode).name.lower() if mode is not None else None
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the air purifier."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Switchbot air purifier to set preset mode %s %s",
|
||||
preset_mode,
|
||||
self._attr_unique_id,
|
||||
)
|
||||
await self.send_api_command(
|
||||
AirPurifierCommands.SET_MODE,
|
||||
parameters={"mode": AirPurifierMode[preset_mode.upper()].value},
|
||||
)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the air purifier."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Switchbot air purifier to set turn on %s %s %s",
|
||||
percentage,
|
||||
preset_mode,
|
||||
self._attr_unique_id,
|
||||
)
|
||||
await self.send_api_command(CommonCommands.ON)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the air purifier."""
|
||||
|
||||
_LOGGER.debug("Switchbot air purifier to set turn off %s", self._attr_unique_id)
|
||||
await self.send_api_command(CommonCommands.OFF)
|
||||
await asyncio.sleep(AFTER_COMMAND_REFRESH)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"default": "mdi:air-purifier",
|
||||
"state": {
|
||||
"off": "mdi:air-purifier-off"
|
||||
},
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"normal": "mdi:fan",
|
||||
"auto": "mdi:auto-mode",
|
||||
"pet": "mdi:paw",
|
||||
"sleep": "mdi:power-sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,5 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"normal": "[%key:common::state::normal%]",
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"pet": "Pet",
|
||||
"sleep": "Sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import DeviceNotFound, ToGrillConfigEntry, ToGrillCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.NUMBER]
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool:
|
||||
|
||||
@@ -2,20 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
|
||||
from bleak.exc import BleakError
|
||||
from togrill_bluetooth.client import Client
|
||||
from togrill_bluetooth.exceptions import DecodeError
|
||||
from togrill_bluetooth.packets import (
|
||||
Packet,
|
||||
PacketA0Notify,
|
||||
PacketA1Notify,
|
||||
PacketA8Write,
|
||||
)
|
||||
from togrill_bluetooth.packets import Packet, PacketA0Notify, PacketA1Notify
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import (
|
||||
@@ -32,15 +25,11 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_PROBE_COUNT
|
||||
|
||||
type ToGrillConfigEntry = ConfigEntry[ToGrillCoordinator]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PacketType = TypeVar("PacketType", bound=Packet)
|
||||
|
||||
|
||||
def get_version_string(packet: PacketA0Notify) -> str:
|
||||
"""Construct a version string from packet data."""
|
||||
@@ -55,7 +44,7 @@ class DeviceFailed(UpdateFailed):
|
||||
"""Update failed due to device disconnected."""
|
||||
|
||||
|
||||
class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Packet]]):
|
||||
class ToGrillCoordinator(DataUpdateCoordinator[dict[int, Packet]]):
|
||||
"""Class to manage fetching data."""
|
||||
|
||||
config_entry: ToGrillConfigEntry
|
||||
@@ -79,7 +68,6 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
self.device_info = DeviceInfo(
|
||||
connections={(CONNECTION_BLUETOOTH, self.address)}
|
||||
)
|
||||
self._packet_listeners: list[Callable[[Packet], None]] = []
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_register_callback(
|
||||
@@ -90,23 +78,6 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_add_packet_listener(
|
||||
self, packet_callback: Callable[[Packet], None]
|
||||
) -> Callable[[], None]:
|
||||
"""Add a listener for a given packet type."""
|
||||
|
||||
def _unregister():
|
||||
self._packet_listeners.remove(packet_callback)
|
||||
|
||||
self._packet_listeners.append(packet_callback)
|
||||
return _unregister
|
||||
|
||||
def async_update_packet_listeners(self, packet: Packet):
|
||||
"""Update all packet listeners."""
|
||||
for listener in self._packet_listeners:
|
||||
listener(packet)
|
||||
|
||||
async def _connect_and_update_registry(self) -> Client:
|
||||
"""Update device registry data."""
|
||||
device = bluetooth.async_ble_device_from_address(
|
||||
@@ -115,12 +86,7 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
if not device:
|
||||
raise DeviceNotFound("Unable to find device")
|
||||
|
||||
try:
|
||||
client = await Client.connect(device, self._notify_callback)
|
||||
except BleakError as exc:
|
||||
self.logger.debug("Connection failed", exc_info=True)
|
||||
raise DeviceNotFound("Unable to connect to device") from exc
|
||||
|
||||
client = await Client.connect(device, self._notify_callback)
|
||||
try:
|
||||
packet_a0 = await client.read(PacketA0Notify)
|
||||
except (BleakError, DecodeError) as exc:
|
||||
@@ -157,30 +123,16 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
self.client = await self._connect_and_update_registry()
|
||||
return self.client
|
||||
|
||||
def get_packet(
|
||||
self, packet_type: type[PacketType], probe=None
|
||||
) -> PacketType | None:
|
||||
"""Get a cached packet of a certain type."""
|
||||
|
||||
if packet := self.data.get((packet_type.type, probe)):
|
||||
assert isinstance(packet, packet_type)
|
||||
return packet
|
||||
return None
|
||||
|
||||
def _notify_callback(self, packet: Packet):
|
||||
probe = getattr(packet, "probe", None)
|
||||
self.data[(packet.type, probe)] = packet
|
||||
self.async_update_packet_listeners(packet)
|
||||
self.data[packet.type] = packet
|
||||
self.async_update_listeners()
|
||||
|
||||
async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]:
|
||||
async def _async_update_data(self) -> dict[int, Packet]:
|
||||
"""Poll the device."""
|
||||
client = await self._get_connected_client()
|
||||
try:
|
||||
await client.request(PacketA0Notify)
|
||||
await client.request(PacketA1Notify)
|
||||
for probe in range(1, self.config_entry.data[CONF_PROBE_COUNT] + 1):
|
||||
await client.write(PacketA8Write(probe=probe))
|
||||
except BleakError as exc:
|
||||
raise DeviceFailed(f"Device failed {exc}") from exc
|
||||
return self.data
|
||||
|
||||
@@ -2,16 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bleak.exc import BleakError
|
||||
from togrill_bluetooth.client import Client
|
||||
from togrill_bluetooth.exceptions import BaseError
|
||||
from togrill_bluetooth.packets import PacketWrite
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LOGGER, ToGrillCoordinator
|
||||
from .coordinator import ToGrillCoordinator
|
||||
|
||||
|
||||
class ToGrillEntity(CoordinatorEntity[ToGrillCoordinator]):
|
||||
@@ -23,27 +16,3 @@ class ToGrillEntity(CoordinatorEntity[ToGrillCoordinator]):
|
||||
"""Initialize coordinator entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
def _get_client(self) -> Client:
|
||||
client = self.coordinator.client
|
||||
if client is None or not client.is_connected:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="disconnected"
|
||||
)
|
||||
return client
|
||||
|
||||
async def _write_packet(self, packet: PacketWrite) -> None:
|
||||
client = self._get_client()
|
||||
try:
|
||||
await client.write(packet)
|
||||
except BleakError as exc:
|
||||
LOGGER.debug("Failed to write", exc_info=True)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="communication_failed"
|
||||
) from exc
|
||||
except BaseError as exc:
|
||||
LOGGER.debug("Failed to write", exc_info=True)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="rejected"
|
||||
) from exc
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Support for event entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from togrill_bluetooth.packets import Packet, PacketA5Notify
|
||||
|
||||
from homeassistant.components.event import EventEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import ToGrillConfigEntry
|
||||
from .const import CONF_PROBE_COUNT
|
||||
from .coordinator import ToGrillCoordinator
|
||||
from .entity import ToGrillEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ToGrillConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up event platform."""
|
||||
async_add_entities(
|
||||
ToGrillEventEntity(config_entry.runtime_data, probe_number=probe_number)
|
||||
for probe_number in range(1, config_entry.data[CONF_PROBE_COUNT] + 1)
|
||||
)
|
||||
|
||||
|
||||
class ToGrillEventEntity(ToGrillEntity, EventEntity):
|
||||
"""Representation of a Hue Event entity from a button resource."""
|
||||
|
||||
def __init__(self, coordinator: ToGrillCoordinator, probe_number: int) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
|
||||
self._attr_translation_key = "event"
|
||||
self._attr_translation_placeholders = {"probe_number": f"{probe_number}"}
|
||||
self._attr_unique_id = f"{coordinator.address}_{probe_number}"
|
||||
self._probe_number = probe_number
|
||||
|
||||
self._attr_event_types: list[str] = [
|
||||
slugify(event.name) for event in PacketA5Notify.Message
|
||||
]
|
||||
|
||||
self.async_on_remove(coordinator.async_add_packet_listener(self._handle_event))
|
||||
|
||||
@callback
|
||||
def _handle_event(self, packet: Packet) -> None:
|
||||
if not isinstance(packet, PacketA5Notify):
|
||||
return
|
||||
|
||||
try:
|
||||
message = PacketA5Notify.Message(packet.message)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if packet.probe != self._probe_number:
|
||||
return
|
||||
|
||||
self._trigger_event(slugify(message.name))
|
||||
@@ -13,7 +13,6 @@
|
||||
"dependencies": ["bluetooth"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/togrill",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["togrill_bluetooth"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["togrill-bluetooth==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
"""Support for number entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from togrill_bluetooth.packets import (
|
||||
PacketA0Notify,
|
||||
PacketA6Write,
|
||||
PacketA8Notify,
|
||||
PacketA301Write,
|
||||
PacketWrite,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import ToGrillConfigEntry
|
||||
from .const import CONF_PROBE_COUNT, MAX_PROBE_COUNT
|
||||
from .coordinator import ToGrillCoordinator
|
||||
from .entity import ToGrillEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class ToGrillNumberEntityDescription(NumberEntityDescription):
|
||||
"""Description of entity."""
|
||||
|
||||
get_value: Callable[[ToGrillCoordinator], float | None]
|
||||
set_packet: Callable[[float], PacketWrite]
|
||||
entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True
|
||||
|
||||
|
||||
def _get_temperature_target_description(
|
||||
probe_number: int,
|
||||
) -> ToGrillNumberEntityDescription:
|
||||
def _set_packet(value: float | None) -> PacketWrite:
|
||||
if value == 0.0:
|
||||
value = None
|
||||
return PacketA301Write(probe=probe_number, target=value)
|
||||
|
||||
def _get_value(coordinator: ToGrillCoordinator) -> float | None:
|
||||
if packet := coordinator.get_packet(PacketA8Notify, probe_number):
|
||||
if packet.alarm_type == PacketA8Notify.AlarmType.TEMPERATURE_TARGET:
|
||||
return packet.temperature_1
|
||||
return None
|
||||
|
||||
return ToGrillNumberEntityDescription(
|
||||
key=f"temperature_target_{probe_number}",
|
||||
translation_key="temperature_target",
|
||||
translation_placeholders={"probe_number": f"{probe_number}"},
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
native_min_value=0,
|
||||
native_max_value=250,
|
||||
mode=NumberMode.BOX,
|
||||
set_packet=_set_packet,
|
||||
get_value=_get_value,
|
||||
entity_supported=lambda x: probe_number <= x[CONF_PROBE_COUNT],
|
||||
)
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS = (
|
||||
*[
|
||||
_get_temperature_target_description(probe_number)
|
||||
for probe_number in range(1, MAX_PROBE_COUNT + 1)
|
||||
],
|
||||
ToGrillNumberEntityDescription(
|
||||
key="alarm_interval",
|
||||
translation_key="alarm_interval",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_min_value=0,
|
||||
native_max_value=15,
|
||||
native_step=5,
|
||||
mode=NumberMode.BOX,
|
||||
set_packet=lambda x: (
|
||||
PacketA6Write(temperature_unit=None, alarm_interval=round(x))
|
||||
),
|
||||
get_value=lambda x: (
|
||||
packet.alarm_interval if (packet := x.get_packet(PacketA0Notify)) else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ToGrillConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up number based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
ToGrillNumber(coordinator, entity_description)
|
||||
for entity_description in ENTITY_DESCRIPTIONS
|
||||
if entity_description.entity_supported(entry.data)
|
||||
)
|
||||
|
||||
|
||||
class ToGrillNumber(ToGrillEntity, NumberEntity):
|
||||
"""Representation of a number."""
|
||||
|
||||
entity_description: ToGrillNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ToGrillCoordinator,
|
||||
entity_description: ToGrillNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{coordinator.address}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the value reported by the number."""
|
||||
return self.entity_description.get_value(self.coordinator)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set value on device."""
|
||||
|
||||
packet = self.entity_description.set_packet(value)
|
||||
await self._write_packet(packet)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user