mirror of
https://github.com/home-assistant/core.git
synced 2025-10-08 19:29:38 +00:00
Compare commits
1 Commits
trigger_ac
...
mqtt-suben
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96c111c96c |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -741,7 +741,7 @@ jobs:
|
||||
- name: Generate partial mypy restore key
|
||||
id: generate-mypy-key
|
||||
run: |
|
||||
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
|
||||
mypy_version=$(cat requirements_test.txt | grep mypy | cut -d '=' -f 3)
|
||||
echo "version=$mypy_version" >> $GITHUB_OUTPUT
|
||||
echo "key=mypy-${{ env.MYPY_CACHE_VERSION }}-$mypy_version-${{
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -1065,8 +1065,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/nilu/ @hfurubotten
|
||||
/homeassistant/components/nina/ @DeerMaximum
|
||||
/tests/components/nina/ @DeerMaximum
|
||||
/homeassistant/components/nintendo_parental/ @pantherale0
|
||||
/tests/components/nintendo_parental/ @pantherale0
|
||||
/homeassistant/components/nissan_leaf/ @filcole
|
||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||
@@ -1413,8 +1411,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sfr_box/ @epenet
|
||||
/homeassistant/components/sftp_storage/ @maretodoric
|
||||
/tests/components/sftp_storage/ @maretodoric
|
||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
|
||||
/tests/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
|
||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
||||
/tests/components/sharkiq/ @JeffResc @funkybunch
|
||||
/homeassistant/components/shell_command/ @home-assistant/core
|
||||
/tests/components/shell_command/ @home-assistant/core
|
||||
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
||||
|
@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)
|
||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
|
||||
|
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"cloud_ceiling": {
|
||||
"default": "mdi:weather-fog"
|
||||
},
|
||||
@@ -37,6 +34,9 @@
|
||||
"thunderstorm_probability_night": {
|
||||
"default": "mdi:weather-lightning"
|
||||
},
|
||||
"translation_key": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"tree_pollen": {
|
||||
"default": "mdi:tree-outline"
|
||||
},
|
||||
|
@@ -1,9 +1,7 @@
|
||||
"""Airgradient Update platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
@@ -15,7 +13,6 @@ from .entity import AirGradientEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -34,7 +31,6 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Representation of Airgradient Update."""
|
||||
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_server_unreachable_logged = False
|
||||
|
||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
@@ -51,27 +47,10 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Return the installed version of the entity."""
|
||||
return self.coordinator.data.measures.firmware_version
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._attr_available
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
try:
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
)
|
||||
)
|
||||
except AirGradientConnectionError:
|
||||
self._attr_latest_version = None
|
||||
self._attr_available = False
|
||||
if not self._server_unreachable_logged:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to AirGradient server to check for updates"
|
||||
)
|
||||
self._server_unreachable_logged = True
|
||||
else:
|
||||
self._server_unreachable_logged = False
|
||||
self._attr_available = True
|
||||
|
@@ -16,12 +16,10 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -114,21 +112,6 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"lux": SensorEntityDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"noise": SensorEntityDescription(
|
||||
key="noise",
|
||||
translation_key="ambient_noise",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
}
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
@@ -41,9 +41,6 @@
|
||||
},
|
||||
"illuminance": {
|
||||
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
|
||||
},
|
||||
"ambient_noise": {
|
||||
"name": "Ambient noise"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,8 +7,6 @@ from typing import Any
|
||||
from pyaprilaire.const import Attribute
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
FAN_AUTO,
|
||||
FAN_ON,
|
||||
PRESET_AWAY,
|
||||
@@ -18,12 +16,7 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
PRECISION_WHOLE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import PRECISION_HALVES, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -239,15 +232,15 @@ class AprilaireClimate(BaseAprilaireEntity, ClimateEntity):
|
||||
cool_setpoint = 0
|
||||
heat_setpoint = 0
|
||||
|
||||
if temperature := kwargs.get(ATTR_TEMPERATURE):
|
||||
if temperature := kwargs.get("temperature"):
|
||||
if self.coordinator.data.get(Attribute.MODE) == 3:
|
||||
cool_setpoint = temperature
|
||||
else:
|
||||
heat_setpoint = temperature
|
||||
else:
|
||||
if target_temp_low := kwargs.get(ATTR_TARGET_TEMP_LOW):
|
||||
if target_temp_low := kwargs.get("target_temp_low"):
|
||||
heat_setpoint = target_temp_low
|
||||
if target_temp_high := kwargs.get(ATTR_TARGET_TEMP_HIGH):
|
||||
if target_temp_high := kwargs.get("target_temp_high"):
|
||||
cool_setpoint = target_temp_high
|
||||
|
||||
if cool_setpoint == 0 and heat_setpoint == 0:
|
||||
|
@@ -7,14 +7,12 @@ from typing import Any
|
||||
from evolutionhttp import BryantEvolutionLocalClient
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -210,24 +208,24 @@ class BryantEvolutionClimate(ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if value := kwargs.get(ATTR_TARGET_TEMP_HIGH):
|
||||
temp = int(value)
|
||||
if kwargs.get("target_temp_high"):
|
||||
temp = int(kwargs["target_temp_high"])
|
||||
if not await self._client.set_cooling_setpoint(temp):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="failed_to_set_clsp"
|
||||
)
|
||||
self._attr_target_temperature_high = temp
|
||||
|
||||
if value := kwargs.get(ATTR_TARGET_TEMP_LOW):
|
||||
temp = int(value)
|
||||
if kwargs.get("target_temp_low"):
|
||||
temp = int(kwargs["target_temp_low"])
|
||||
if not await self._client.set_heating_setpoint(temp):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="failed_to_set_htsp"
|
||||
)
|
||||
self._attr_target_temperature_low = temp
|
||||
|
||||
if value := kwargs.get(ATTR_TEMPERATURE):
|
||||
temp = int(value)
|
||||
if kwargs.get("temperature"):
|
||||
temp = int(kwargs["temperature"])
|
||||
fn = (
|
||||
self._client.set_heating_setpoint
|
||||
if self.hvac_mode == HVACMode.HEAT
|
||||
|
@@ -169,7 +169,7 @@ class CalendarEventListener:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
job: HassJob[..., Coroutine[Any, Any, None] | Any],
|
||||
job: HassJob[..., Coroutine[Any, Any, None]],
|
||||
trigger_data: dict[str, Any],
|
||||
fetcher: QueuedEventFetcher,
|
||||
) -> None:
|
||||
|
@@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
|
||||
from .utils import DeviceType, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -30,19 +29,23 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data["alarm_zones"])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitVedoBinarySensorEntity(
|
||||
coordinator, device, config_entry.entry_id
|
||||
)
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitVedoBinarySensorEntity(
|
||||
|
@@ -7,21 +7,14 @@ from typing import Any, cast
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -36,20 +29,22 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, COVER)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[COVER])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[COVER].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
"""Cover device."""
|
||||
@@ -67,6 +62,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
super().__init__(coordinator, device, config_entry_entry_id)
|
||||
# Device doesn't provide a status so we assume UNKNOWN at first startup
|
||||
self._last_action: int | None = None
|
||||
self._last_state: str | None = None
|
||||
|
||||
def _current_action(self, action: str) -> bool:
|
||||
"""Return the current cover action."""
|
||||
@@ -102,6 +98,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@bridge_api_call
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
@@ -127,10 +124,5 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
if state.state == STATE_CLOSED:
|
||||
self._last_action = STATE_COVER.index(STATE_CLOSING)
|
||||
if state.state == STATE_OPEN:
|
||||
self._last_action = STATE_COVER.index(STATE_OPENING)
|
||||
|
||||
self._attr_is_closed = state.state == STATE_CLOSED
|
||||
if last_state := await self.async_get_last_state():
|
||||
self._last_state = last_state.state
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -27,20 +27,22 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, LIGHT)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[LIGHT])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[LIGHT].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):
|
||||
"""Light device."""
|
||||
|
@@ -20,7 +20,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -66,23 +65,25 @@ async def async_setup_bridge_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[OTHER])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitBridgeSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_BRIDGE_TYPES
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, OTHER)
|
||||
for device in coordinator.data[OTHER].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
async def async_setup_vedo_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -93,23 +94,25 @@ async def async_setup_vedo_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data["alarm_zones"])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitVedoSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_VEDO_TYPES
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):
|
||||
"""Sensor device."""
|
||||
|
@@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -28,21 +28,36 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
entities: list[ComelitSwitchEntity] = []
|
||||
entities.extend(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
for device in coordinator.data[IRRIGATION].values()
|
||||
)
|
||||
entities.extend(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[OTHER].values()
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
known_devices: dict[str, set[int]] = {
|
||||
dev_type: set() for dev_type in (IRRIGATION, OTHER)
|
||||
}
|
||||
|
||||
def _check_device() -> None:
|
||||
for dev_type in (IRRIGATION, OTHER):
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, dev_type)
|
||||
current_devices = set(coordinator.data[dev_type])
|
||||
new_devices = current_devices - known_devices[dev_type]
|
||||
if new_devices:
|
||||
known_devices[dev_type].update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
|
||||
"""Switch device."""
|
||||
|
@@ -4,11 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiocomelit.api import (
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
@@ -23,11 +19,8 @@ from homeassistant.helpers import (
|
||||
)
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import ComelitBaseCoordinator
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
|
||||
DeviceType = ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject
|
||||
|
||||
|
||||
async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Return a new aiohttp session."""
|
||||
@@ -120,41 +113,3 @@ def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
|
||||
return cmd_wrapper
|
||||
|
||||
|
||||
def new_device_listener(
|
||||
coordinator: ComelitBaseCoordinator,
|
||||
new_devices_callback: Callable[
|
||||
[
|
||||
list[
|
||||
ComelitSerialBridgeObject
|
||||
| ComelitVedoAreaObject
|
||||
| ComelitVedoZoneObject
|
||||
],
|
||||
str,
|
||||
],
|
||||
None,
|
||||
],
|
||||
data_type: str,
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to coordinator updates to check for new devices."""
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_devices() -> None:
|
||||
"""Check for new devices and call callback with any new monitors."""
|
||||
if not coordinator.data:
|
||||
return
|
||||
|
||||
new_devices: list[DeviceType] = []
|
||||
for _id in coordinator.data[data_type]:
|
||||
if _id not in known_devices:
|
||||
known_devices.add(_id)
|
||||
new_devices.append(coordinator.data[data_type][_id])
|
||||
|
||||
if new_devices:
|
||||
new_devices_callback(new_devices, data_type)
|
||||
|
||||
# Check for devices immediately
|
||||
_check_devices()
|
||||
|
||||
return coordinator.async_add_listener(_check_devices)
|
||||
|
@@ -38,30 +38,22 @@ from home_assistant_intents import (
|
||||
ErrorKey,
|
||||
FuzzyConfig,
|
||||
FuzzyLanguageResponses,
|
||||
LanguageScores,
|
||||
get_fuzzy_config,
|
||||
get_fuzzy_language,
|
||||
get_intents,
|
||||
get_language_scores,
|
||||
get_languages,
|
||||
)
|
||||
import yaml
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_listen_entity_updates,
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL
|
||||
from homeassistant.core import (
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
@@ -200,7 +192,7 @@ class IntentCache:
|
||||
|
||||
|
||||
async def async_setup_default_agent(
|
||||
hass: HomeAssistant,
|
||||
hass: core.HomeAssistant,
|
||||
entity_component: EntityComponent[ConversationEntity],
|
||||
config_intents: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -209,13 +201,15 @@ async def async_setup_default_agent(
|
||||
await entity_component.async_add_entities([agent])
|
||||
await get_agent_manager(hass).async_setup_default_agent(agent)
|
||||
|
||||
@callback
|
||||
def async_entity_state_listener(event: Event[EventStateChangedData]) -> None:
|
||||
@core.callback
|
||||
def async_entity_state_listener(
|
||||
event: core.Event[core.EventStateChangedData],
|
||||
) -> None:
|
||||
"""Set expose flag on new entities."""
|
||||
async_should_expose(hass, DOMAIN, event.data["entity_id"])
|
||||
|
||||
@callback
|
||||
def async_hass_started(hass: HomeAssistant) -> None:
|
||||
@core.callback
|
||||
def async_hass_started(hass: core.HomeAssistant) -> None:
|
||||
"""Set expose flag on all entities."""
|
||||
for state in hass.states.async_all():
|
||||
async_should_expose(hass, DOMAIN, state.entity_id)
|
||||
@@ -230,7 +224,9 @@ class DefaultAgent(ConversationEntity):
|
||||
_attr_name = "Home Assistant"
|
||||
_attr_supported_features = ConversationEntityFeature.CONTROL
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_intents: dict[str, Any]) -> None:
|
||||
def __init__(
|
||||
self, hass: core.HomeAssistant, config_intents: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize the default agent."""
|
||||
self.hass = hass
|
||||
self._lang_intents: dict[str, LanguageIntents | object] = {}
|
||||
@@ -263,7 +259,7 @@ class DefaultAgent(ConversationEntity):
|
||||
"""Return a list of supported languages."""
|
||||
return get_languages()
|
||||
|
||||
@callback
|
||||
@core.callback
|
||||
def _filter_entity_registry_changes(
|
||||
self, event_data: er.EventEntityRegistryUpdatedData
|
||||
) -> bool:
|
||||
@@ -272,12 +268,12 @@ class DefaultAgent(ConversationEntity):
|
||||
field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
|
||||
)
|
||||
|
||||
@callback
|
||||
def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
|
||||
@core.callback
|
||||
def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool:
|
||||
"""Filter state changed events."""
|
||||
return not event_data["old_state"] or not event_data["new_state"]
|
||||
|
||||
@callback
|
||||
@core.callback
|
||||
def _listen_clear_slot_list(self) -> None:
|
||||
"""Listen for changes that can invalidate slot list."""
|
||||
assert self._unsub_clear_slot_list is None
|
||||
@@ -346,81 +342,6 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
return result
|
||||
|
||||
async def async_debug_recognize(
|
||||
self, user_input: ConversationInput
|
||||
) -> dict[str, Any] | None:
|
||||
"""Debug recognize from user input."""
|
||||
result_dict: dict[str, Any] | None = None
|
||||
|
||||
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
|
||||
result_dict = {
|
||||
# Matched a user-defined sentence trigger.
|
||||
# We can't provide the response here without executing the
|
||||
# trigger.
|
||||
"match": True,
|
||||
"source": "trigger",
|
||||
"sentence_template": trigger_result.sentence_template or "",
|
||||
}
|
||||
elif intent_result := await self.async_recognize_intent(user_input):
|
||||
successful_match = not intent_result.unmatched_entities
|
||||
result_dict = {
|
||||
# Name of the matching intent (or the closest)
|
||||
"intent": {
|
||||
"name": intent_result.intent.name,
|
||||
},
|
||||
# Slot values that would be received by the intent
|
||||
"slots": { # direct access to values
|
||||
entity_key: entity.text or entity.value
|
||||
for entity_key, entity in intent_result.entities.items()
|
||||
},
|
||||
# Extra slot details, such as the originally matched text
|
||||
"details": {
|
||||
entity_key: {
|
||||
"name": entity.name,
|
||||
"value": entity.value,
|
||||
"text": entity.text,
|
||||
}
|
||||
for entity_key, entity in intent_result.entities.items()
|
||||
},
|
||||
# Entities/areas/etc. that would be targeted
|
||||
"targets": {},
|
||||
# True if match was successful
|
||||
"match": successful_match,
|
||||
# Text of the sentence template that matched (or was closest)
|
||||
"sentence_template": "",
|
||||
# When match is incomplete, this will contain the best slot guesses
|
||||
"unmatched_slots": _get_unmatched_slots(intent_result),
|
||||
# True if match was not exact
|
||||
"fuzzy_match": False,
|
||||
}
|
||||
|
||||
if successful_match:
|
||||
result_dict["targets"] = {
|
||||
state.entity_id: {"matched": is_matched}
|
||||
for state, is_matched in _get_debug_targets(
|
||||
self.hass, intent_result
|
||||
)
|
||||
}
|
||||
|
||||
if intent_result.intent_sentence is not None:
|
||||
result_dict["sentence_template"] = intent_result.intent_sentence.text
|
||||
|
||||
if intent_result.intent_metadata:
|
||||
# Inspect metadata to determine if this matched a custom sentence
|
||||
if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE):
|
||||
result_dict["source"] = "custom"
|
||||
result_dict["file"] = intent_result.intent_metadata.get(
|
||||
METADATA_CUSTOM_FILE
|
||||
)
|
||||
else:
|
||||
result_dict["source"] = "builtin"
|
||||
|
||||
result_dict["fuzzy_match"] = intent_result.intent_metadata.get(
|
||||
METADATA_FUZZY_MATCH, False
|
||||
)
|
||||
|
||||
return result_dict
|
||||
|
||||
async def _async_handle_message(
|
||||
self,
|
||||
user_input: ConversationInput,
|
||||
@@ -969,7 +890,7 @@ class DefaultAgent(ConversationEntity):
|
||||
) -> str:
|
||||
# Get first matched or unmatched state.
|
||||
# This is available in the response template as "state".
|
||||
state1: State | None = None
|
||||
state1: core.State | None = None
|
||||
if intent_response.matched_states:
|
||||
state1 = intent_response.matched_states[0]
|
||||
elif intent_response.unmatched_states:
|
||||
@@ -1607,10 +1528,6 @@ class DefaultAgent(ConversationEntity):
|
||||
return None
|
||||
return response
|
||||
|
||||
async def async_get_language_scores(self) -> dict[str, LanguageScores]:
|
||||
"""Get support scores per language."""
|
||||
return await self.hass.async_add_executor_job(get_language_scores)
|
||||
|
||||
|
||||
def _make_error_result(
|
||||
language: str,
|
||||
@@ -1672,7 +1589,7 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
|
||||
|
||||
def _get_match_error_response(
|
||||
hass: HomeAssistant,
|
||||
hass: core.HomeAssistant,
|
||||
match_error: intent.MatchFailedError,
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when target matching fails."""
|
||||
@@ -1807,75 +1724,3 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No
|
||||
elif isinstance(expression, ListReference):
|
||||
# {list}
|
||||
list_names.add(expression.slot_name)
|
||||
|
||||
|
||||
def _get_debug_targets(
|
||||
hass: HomeAssistant,
|
||||
result: RecognizeResult,
|
||||
) -> Iterable[tuple[State, bool]]:
|
||||
"""Yield state/is_matched pairs for a hassil recognition."""
|
||||
entities = result.entities
|
||||
|
||||
name: str | None = None
|
||||
area_name: str | None = None
|
||||
domains: set[str] | None = None
|
||||
device_classes: set[str] | None = None
|
||||
state_names: set[str] | None = None
|
||||
|
||||
if "name" in entities:
|
||||
name = str(entities["name"].value)
|
||||
|
||||
if "area" in entities:
|
||||
area_name = str(entities["area"].value)
|
||||
|
||||
if "domain" in entities:
|
||||
domains = set(cv.ensure_list(entities["domain"].value))
|
||||
|
||||
if "device_class" in entities:
|
||||
device_classes = set(cv.ensure_list(entities["device_class"].value))
|
||||
|
||||
if "state" in entities:
|
||||
# HassGetState only
|
||||
state_names = set(cv.ensure_list(entities["state"].value))
|
||||
|
||||
if (
|
||||
(name is None)
|
||||
and (area_name is None)
|
||||
and (not domains)
|
||||
and (not device_classes)
|
||||
and (not state_names)
|
||||
):
|
||||
# Avoid "matching" all entities when there is no filter
|
||||
return
|
||||
|
||||
states = intent.async_match_states(
|
||||
hass,
|
||||
name=name,
|
||||
area_name=area_name,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
)
|
||||
|
||||
for state in states:
|
||||
# For queries, a target is "matched" based on its state
|
||||
is_matched = (state_names is None) or (state.state in state_names)
|
||||
yield state, is_matched
|
||||
|
||||
|
||||
def _get_unmatched_slots(
|
||||
result: RecognizeResult,
|
||||
) -> dict[str, str | int | float]:
|
||||
"""Return a dict of unmatched text/range slot entities."""
|
||||
unmatched_slots: dict[str, str | int | float] = {}
|
||||
for entity in result.unmatched_entities_list:
|
||||
if isinstance(entity, UnmatchedTextEntity):
|
||||
if entity.text == MISSING_ENTITY:
|
||||
# Don't report <missing> since these are just missing context
|
||||
# slots.
|
||||
continue
|
||||
|
||||
unmatched_slots[entity.name] = entity.text
|
||||
elif isinstance(entity, UnmatchedRangeEntity):
|
||||
unmatched_slots[entity.name] = entity.value
|
||||
|
||||
return unmatched_slots
|
||||
|
@@ -2,16 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from hassil.recognize import MISSING_ENTITY, RecognizeResult
|
||||
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
||||
from home_assistant_intents import get_language_scores
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http, websocket_api
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv, intent
|
||||
from homeassistant.util import language as language_util
|
||||
|
||||
from .agent_manager import (
|
||||
@@ -21,6 +26,11 @@ from .agent_manager import (
|
||||
get_agent_manager,
|
||||
)
|
||||
from .const import DATA_COMPONENT
|
||||
from .default_agent import (
|
||||
METADATA_CUSTOM_FILE,
|
||||
METADATA_CUSTOM_SENTENCE,
|
||||
METADATA_FUZZY_MATCH,
|
||||
)
|
||||
from .entity import ConversationEntity
|
||||
from .models import ConversationInput
|
||||
|
||||
@@ -196,12 +206,150 @@ async def websocket_hass_agent_debug(
|
||||
language=msg.get("language", hass.config.language),
|
||||
agent_id=agent.entity_id,
|
||||
)
|
||||
result_dict = await agent.async_debug_recognize(user_input)
|
||||
result_dict: dict[str, Any] | None = None
|
||||
|
||||
if trigger_result := await agent.async_recognize_sentence_trigger(user_input):
|
||||
result_dict = {
|
||||
# Matched a user-defined sentence trigger.
|
||||
# We can't provide the response here without executing the
|
||||
# trigger.
|
||||
"match": True,
|
||||
"source": "trigger",
|
||||
"sentence_template": trigger_result.sentence_template or "",
|
||||
}
|
||||
elif intent_result := await agent.async_recognize_intent(user_input):
|
||||
successful_match = not intent_result.unmatched_entities
|
||||
result_dict = {
|
||||
# Name of the matching intent (or the closest)
|
||||
"intent": {
|
||||
"name": intent_result.intent.name,
|
||||
},
|
||||
# Slot values that would be received by the intent
|
||||
"slots": { # direct access to values
|
||||
entity_key: entity.text or entity.value
|
||||
for entity_key, entity in intent_result.entities.items()
|
||||
},
|
||||
# Extra slot details, such as the originally matched text
|
||||
"details": {
|
||||
entity_key: {
|
||||
"name": entity.name,
|
||||
"value": entity.value,
|
||||
"text": entity.text,
|
||||
}
|
||||
for entity_key, entity in intent_result.entities.items()
|
||||
},
|
||||
# Entities/areas/etc. that would be targeted
|
||||
"targets": {},
|
||||
# True if match was successful
|
||||
"match": successful_match,
|
||||
# Text of the sentence template that matched (or was closest)
|
||||
"sentence_template": "",
|
||||
# When match is incomplete, this will contain the best slot guesses
|
||||
"unmatched_slots": _get_unmatched_slots(intent_result),
|
||||
# True if match was not exact
|
||||
"fuzzy_match": False,
|
||||
}
|
||||
|
||||
if successful_match:
|
||||
result_dict["targets"] = {
|
||||
state.entity_id: {"matched": is_matched}
|
||||
for state, is_matched in _get_debug_targets(hass, intent_result)
|
||||
}
|
||||
|
||||
if intent_result.intent_sentence is not None:
|
||||
result_dict["sentence_template"] = intent_result.intent_sentence.text
|
||||
|
||||
if intent_result.intent_metadata:
|
||||
# Inspect metadata to determine if this matched a custom sentence
|
||||
if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE):
|
||||
result_dict["source"] = "custom"
|
||||
result_dict["file"] = intent_result.intent_metadata.get(
|
||||
METADATA_CUSTOM_FILE
|
||||
)
|
||||
else:
|
||||
result_dict["source"] = "builtin"
|
||||
|
||||
result_dict["fuzzy_match"] = intent_result.intent_metadata.get(
|
||||
METADATA_FUZZY_MATCH, False
|
||||
)
|
||||
|
||||
result_dicts.append(result_dict)
|
||||
|
||||
connection.send_result(msg["id"], {"results": result_dicts})
|
||||
|
||||
|
||||
def _get_debug_targets(
|
||||
hass: HomeAssistant,
|
||||
result: RecognizeResult,
|
||||
) -> Iterable[tuple[State, bool]]:
|
||||
"""Yield state/is_matched pairs for a hassil recognition."""
|
||||
entities = result.entities
|
||||
|
||||
name: str | None = None
|
||||
area_name: str | None = None
|
||||
domains: set[str] | None = None
|
||||
device_classes: set[str] | None = None
|
||||
state_names: set[str] | None = None
|
||||
|
||||
if "name" in entities:
|
||||
name = str(entities["name"].value)
|
||||
|
||||
if "area" in entities:
|
||||
area_name = str(entities["area"].value)
|
||||
|
||||
if "domain" in entities:
|
||||
domains = set(cv.ensure_list(entities["domain"].value))
|
||||
|
||||
if "device_class" in entities:
|
||||
device_classes = set(cv.ensure_list(entities["device_class"].value))
|
||||
|
||||
if "state" in entities:
|
||||
# HassGetState only
|
||||
state_names = set(cv.ensure_list(entities["state"].value))
|
||||
|
||||
if (
|
||||
(name is None)
|
||||
and (area_name is None)
|
||||
and (not domains)
|
||||
and (not device_classes)
|
||||
and (not state_names)
|
||||
):
|
||||
# Avoid "matching" all entities when there is no filter
|
||||
return
|
||||
|
||||
states = intent.async_match_states(
|
||||
hass,
|
||||
name=name,
|
||||
area_name=area_name,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
)
|
||||
|
||||
for state in states:
|
||||
# For queries, a target is "matched" based on its state
|
||||
is_matched = (state_names is None) or (state.state in state_names)
|
||||
yield state, is_matched
|
||||
|
||||
|
||||
def _get_unmatched_slots(
|
||||
result: RecognizeResult,
|
||||
) -> dict[str, str | int | float]:
|
||||
"""Return a dict of unmatched text/range slot entities."""
|
||||
unmatched_slots: dict[str, str | int | float] = {}
|
||||
for entity in result.unmatched_entities_list:
|
||||
if isinstance(entity, UnmatchedTextEntity):
|
||||
if entity.text == MISSING_ENTITY:
|
||||
# Don't report <missing> since these are just missing context
|
||||
# slots.
|
||||
continue
|
||||
|
||||
unmatched_slots[entity.name] = entity.text
|
||||
elif isinstance(entity, UnmatchedRangeEntity):
|
||||
unmatched_slots[entity.name] = entity.value
|
||||
|
||||
return unmatched_slots
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "conversation/agent/homeassistant/language_scores",
|
||||
@@ -216,13 +364,10 @@ async def websocket_hass_agent_language_scores(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get support scores per language."""
|
||||
agent = get_agent_manager(hass).default_agent
|
||||
assert agent is not None
|
||||
|
||||
language = msg.get("language", hass.config.language)
|
||||
country = msg.get("country", hass.config.country)
|
||||
|
||||
scores = await agent.async_get_language_scores()
|
||||
scores = await hass.async_add_executor_job(get_language_scores)
|
||||
matching_langs = language_util.matches(language, scores.keys(), country=country)
|
||||
preferred_lang = matching_langs[0] if matching_langs else language
|
||||
result = {
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.11.3"]
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==41.13.0",
|
||||
"aioesphomeapi==41.12.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.4.0"
|
||||
],
|
||||
|
@@ -29,12 +29,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_TENTHS,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import ATTR_MODE, PRECISION_TENTHS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -248,7 +243,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new target temperature."""
|
||||
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
temperature = kwargs["temperature"]
|
||||
|
||||
if (until := kwargs.get("until")) is None:
|
||||
if self._evo_device.mode == EvoZoneMode.TEMPORARY_OVERRIDE:
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.82", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.81", "babel==2.15.0"]
|
||||
}
|
||||
|
@@ -456,7 +456,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
||||
return self._available
|
||||
|
||||
@ha_callback
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event."""
|
||||
if state := self.hass.states.get(self.entity_id):
|
||||
@@ -725,7 +725,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
self._entry_title = entry_title
|
||||
self.iid_storage = iid_storage
|
||||
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def pair(
|
||||
self, client_username_bytes: bytes, client_public: str, client_permissions: int
|
||||
) -> bool:
|
||||
@@ -735,7 +735,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
async_dismiss_setup_message(self.hass, self.entry_id)
|
||||
return cast(bool, success)
|
||||
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def unpair(self, client_uuid: UUID) -> None:
|
||||
"""Override super function to show setup message if unpaired."""
|
||||
super().unpair(client_uuid)
|
||||
|
@@ -71,7 +71,7 @@ class HomeDoorbellAccessory(HomeAccessory):
|
||||
self.async_update_doorbell_state(None, state)
|
||||
|
||||
@ha_callback
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def run(self) -> None:
|
||||
"""Handle doorbell event."""
|
||||
if self._char_doorbell_detected:
|
||||
|
@@ -219,7 +219,7 @@ class AirPurifier(Fan):
|
||||
return preset_mode.lower() != "auto"
|
||||
|
||||
@callback
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
||||
|
@@ -229,7 +229,7 @@ class Camera(HomeDoorbellAccessory, PyhapCamera): # type: ignore[misc]
|
||||
)
|
||||
self._async_update_motion_state(None, state)
|
||||
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@callback
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
@@ -127,7 +127,7 @@ class GarageDoorOpener(HomeAccessory):
|
||||
self.async_update_state(state)
|
||||
|
||||
@callback
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
||||
|
@@ -178,7 +178,7 @@ class HumidifierDehumidifier(HomeAccessory):
|
||||
self._async_update_current_humidity(humidity_state)
|
||||
|
||||
@callback
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
||||
|
@@ -108,7 +108,7 @@ class DeviceTriggerAccessory(HomeAccessory):
|
||||
_LOGGER.log,
|
||||
)
|
||||
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@callback
|
||||
def run(self) -> None:
|
||||
"""Run the accessory."""
|
||||
|
@@ -41,12 +41,16 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
||||
self._expected_connected = False
|
||||
self._height: int | None = None
|
||||
|
||||
@callback
|
||||
def async_update_data() -> None:
|
||||
self.async_set_updated_data(self._height)
|
||||
|
||||
self._debouncer = Debouncer(
|
||||
hass=self.hass,
|
||||
logger=_LOGGER,
|
||||
cooldown=UPDATE_DEBOUNCE_TIME,
|
||||
immediate=True,
|
||||
function=callback(lambda: self.async_set_updated_data(self._height)),
|
||||
function=async_update_data,
|
||||
)
|
||||
|
||||
async def async_connect(self) -> bool:
|
||||
|
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.1.2"]
|
||||
"requirements": ["pylamarzocco==2.1.1"]
|
||||
}
|
||||
|
@@ -1,36 +1 @@
|
||||
"""The london_underground component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN as DOMAIN
|
||||
from .coordinator import LondonTubeCoordinator, LondonUndergroundConfigEntry, TubeData
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: LondonUndergroundConfigEntry
|
||||
) -> bool:
|
||||
"""Set up London Underground from a config entry."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
data = TubeData(session)
|
||||
coordinator = LondonTubeCoordinator(hass, data, config_entry=entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
# Forward the setup to the sensor platform
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: LondonUndergroundConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@@ -1,152 +0,0 @@
|
||||
"""Config flow for London Underground integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from london_tube_status import TubeData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_LINE, DEFAULT_LINES, DOMAIN, TUBE_LINES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LondonUndergroundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for London Underground."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
_: ConfigEntry,
|
||||
) -> LondonUndergroundOptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return LondonUndergroundOptionsFlow()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
data = TubeData(session)
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await data.update()
|
||||
except TimeoutError:
|
||||
errors["base"] = "timeout_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="London Underground",
|
||||
data={},
|
||||
options={CONF_LINE: user_input.get(CONF_LINE, DEFAULT_LINES)},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LINE,
|
||||
default=DEFAULT_LINES,
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=TUBE_LINES,
|
||||
multiple=True,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: ConfigType) -> ConfigFlowResult:
|
||||
"""Handle import from configuration.yaml."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
data = TubeData(session)
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await data.update()
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error trying to connect before importing config, aborting import "
|
||||
)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
_LOGGER.warning(
|
||||
"Importing London Underground config from configuration.yaml: %s",
|
||||
import_data,
|
||||
)
|
||||
# Extract lines from the sensor platform config
|
||||
lines = import_data.get(CONF_LINE, DEFAULT_LINES)
|
||||
if "London Overground" in lines:
|
||||
_LOGGER.warning(
|
||||
"London Overground was removed from the configuration as the line has been divided and renamed"
|
||||
)
|
||||
lines.remove("London Overground")
|
||||
return self.async_create_entry(
|
||||
title="London Underground",
|
||||
data={},
|
||||
options={CONF_LINE: import_data.get(CONF_LINE, DEFAULT_LINES)},
|
||||
)
|
||||
|
||||
|
||||
class LondonUndergroundOptionsFlow(OptionsFlowWithReload):
|
||||
"""Handle options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
_LOGGER.debug(
|
||||
"Updating london underground with options flow user_input: %s",
|
||||
user_input,
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={CONF_LINE: user_input[CONF_LINE]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LINE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_LINE,
|
||||
self.config_entry.data.get(CONF_LINE, DEFAULT_LINES),
|
||||
),
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=TUBE_LINES,
|
||||
multiple=True,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
@@ -6,6 +6,7 @@ DOMAIN = "london_underground"
|
||||
|
||||
CONF_LINE = "line"
|
||||
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
TUBE_LINES = [
|
||||
@@ -17,7 +18,7 @@ TUBE_LINES = [
|
||||
"Elizabeth line",
|
||||
"Hammersmith & City",
|
||||
"Jubilee",
|
||||
"London Overground", # no longer supported
|
||||
"London Overground",
|
||||
"Metropolitan",
|
||||
"Northern",
|
||||
"Piccadilly",
|
||||
@@ -30,20 +31,3 @@ TUBE_LINES = [
|
||||
"Weaver",
|
||||
"Windrush",
|
||||
]
|
||||
|
||||
# Default lines to monitor if none selected
|
||||
DEFAULT_LINES = [
|
||||
"Bakerloo",
|
||||
"Central",
|
||||
"Circle",
|
||||
"District",
|
||||
"DLR",
|
||||
"Elizabeth line",
|
||||
"Hammersmith & City",
|
||||
"Jubilee",
|
||||
"Metropolitan",
|
||||
"Northern",
|
||||
"Piccadilly",
|
||||
"Victoria",
|
||||
"Waterloo & City",
|
||||
]
|
||||
|
@@ -8,7 +8,6 @@ from typing import cast
|
||||
|
||||
from london_tube_status import TubeData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
@@ -16,23 +15,16 @@ from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LondonUndergroundConfigEntry = ConfigEntry[LondonTubeCoordinator]
|
||||
|
||||
|
||||
class LondonTubeCoordinator(DataUpdateCoordinator[dict[str, dict[str, str]]]):
|
||||
"""London Underground sensor coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
data: TubeData,
|
||||
config_entry: LondonUndergroundConfigEntry,
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, data: TubeData) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
config_entry=None,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
@@ -2,12 +2,9 @@
|
||||
"domain": "london_underground",
|
||||
"name": "London Underground",
|
||||
"codeowners": ["@jpbede"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/london_underground",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["london_tube_status"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["london-tube-status==0.5"],
|
||||
"single_config_entry": true
|
||||
"requirements": ["london-tube-status==0.5"]
|
||||
}
|
||||
|
@@ -5,26 +5,23 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from london_tube_status import TubeData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_LINE, DOMAIN, TUBE_LINES
|
||||
from .coordinator import LondonTubeCoordinator, LondonUndergroundConfigEntry
|
||||
from .const import CONF_LINE, TUBE_LINES
|
||||
from .coordinator import LondonTubeCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,54 +38,18 @@ async def async_setup_platform(
|
||||
) -> None:
|
||||
"""Set up the Tube sensor."""
|
||||
|
||||
# If configuration.yaml config exists, trigger the import flow.
|
||||
# If the config entry already exists, this will not be triggered as only one config is allowed.
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "London Underground",
|
||||
},
|
||||
)
|
||||
return
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
"deprecated_yaml",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "London Underground",
|
||||
},
|
||||
)
|
||||
data = TubeData(session)
|
||||
coordinator = LondonTubeCoordinator(hass, data)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LondonUndergroundConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the London Underground sensor from config entry."""
|
||||
if not coordinator.last_update_success:
|
||||
raise PlatformNotReady
|
||||
|
||||
async_add_entities(
|
||||
LondonTubeSensor(entry.runtime_data, line) for line in entry.options[CONF_LINE]
|
||||
LondonTubeSensor(coordinator, line) for line in config[CONF_LINE]
|
||||
)
|
||||
|
||||
|
||||
@@ -97,21 +58,11 @@ class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity):
|
||||
|
||||
_attr_attribution = "Powered by TfL Open Data"
|
||||
_attr_icon = "mdi:subway"
|
||||
_attr_has_entity_name = True # Use modern entity naming
|
||||
|
||||
def __init__(self, coordinator: LondonTubeCoordinator, name: str) -> None:
|
||||
"""Initialize the London Underground sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
# Add unique_id for proper entity registry
|
||||
self._attr_unique_id = f"tube_{name.lower().replace(' ', '_')}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, DOMAIN)},
|
||||
name="London Underground",
|
||||
manufacturer="Transport for London",
|
||||
model="Tube Status",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up London Underground",
|
||||
"description": "Select which tube lines you want to monitor",
|
||||
"data": {
|
||||
"line": "Tube lines"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure London Underground",
|
||||
"description": "[%key:component::london_underground::config::step::user::description%]",
|
||||
"data": {
|
||||
"line": "[%key:component::london_underground::config::step::user::data::line%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue": {
|
||||
"title": "London Underground YAML configuration deprecated",
|
||||
"description": "Configuring London Underground using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an error occurred when trying to connect to the Transport for London API. Please restart Home Assistant to try again, or remove the existing YAML configuration and set the integration up via the UI."
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -24,13 +23,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
|
||||
from . import async_get_config_entry_implementation
|
||||
from .application_credentials import authorization_server_context
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_ACCESS_TOKEN, CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
|
||||
from .coordinator import TokenManager, mcp_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -48,17 +41,9 @@ MCP_DISCOVERY_HEADERS = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class OAuthConfig:
|
||||
"""Class to hold OAuth configuration."""
|
||||
|
||||
authorization_server: AuthorizationServer
|
||||
scopes: list[str] | None = None
|
||||
|
||||
|
||||
async def async_discover_oauth_config(
|
||||
hass: HomeAssistant, mcp_server_url: str
|
||||
) -> OAuthConfig:
|
||||
) -> AuthorizationServer:
|
||||
"""Discover the OAuth configuration for the MCP server.
|
||||
|
||||
This implements the functionality in the MCP spec for discovery. If the MCP server URL
|
||||
@@ -80,12 +65,10 @@ async def async_discover_oauth_config(
|
||||
except httpx.HTTPStatusError as error:
|
||||
if error.response.status_code == 404:
|
||||
_LOGGER.info("Authorization Server Metadata not found, using default paths")
|
||||
return OAuthConfig(
|
||||
authorization_server=AuthorizationServer(
|
||||
return AuthorizationServer(
|
||||
authorize_url=str(parsed_url.with_path("/authorize")),
|
||||
token_url=str(parsed_url.with_path("/token")),
|
||||
)
|
||||
)
|
||||
raise CannotConnect from error
|
||||
except httpx.HTTPError as error:
|
||||
_LOGGER.info("Cannot discover OAuth configuration: %s", error)
|
||||
@@ -98,15 +81,9 @@ async def async_discover_oauth_config(
|
||||
authorize_url = str(parsed_url.with_path(authorize_url))
|
||||
if token_url.startswith("/"):
|
||||
token_url = str(parsed_url.with_path(token_url))
|
||||
# We have no way to know the minimum set of scopes needed, so request
|
||||
# all of them and let the user limit during the authorization step.
|
||||
scopes = data.get("scopes_supported")
|
||||
return OAuthConfig(
|
||||
authorization_server=AuthorizationServer(
|
||||
return AuthorizationServer(
|
||||
authorize_url=authorize_url,
|
||||
token_url=token_url,
|
||||
),
|
||||
scopes=scopes,
|
||||
)
|
||||
|
||||
|
||||
@@ -153,7 +130,6 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Initialize the config flow."""
|
||||
super().__init__()
|
||||
self.data: dict[str, Any] = {}
|
||||
self.oauth_config: OAuthConfig | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -194,7 +170,7 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
to find the OAuth medata then run the OAuth authentication flow.
|
||||
"""
|
||||
try:
|
||||
oauth_config = await async_discover_oauth_config(
|
||||
authorization_server = await async_discover_oauth_config(
|
||||
self.hass, self.data[CONF_URL]
|
||||
)
|
||||
except TimeoutConnectError:
|
||||
@@ -205,13 +181,11 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
else:
|
||||
_LOGGER.info("OAuth configuration: %s", oauth_config)
|
||||
self.oauth_config = oauth_config
|
||||
_LOGGER.info("OAuth configuration: %s", authorization_server)
|
||||
self.data.update(
|
||||
{
|
||||
CONF_AUTHORIZATION_URL: oauth_config.authorization_server.authorize_url,
|
||||
CONF_TOKEN_URL: oauth_config.authorization_server.token_url,
|
||||
CONF_SCOPE: oauth_config.scopes,
|
||||
CONF_AUTHORIZATION_URL: authorization_server.authorize_url,
|
||||
CONF_TOKEN_URL: authorization_server.token_url,
|
||||
}
|
||||
)
|
||||
return await self.async_step_credentials_choice()
|
||||
@@ -223,15 +197,6 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self.data[CONF_TOKEN_URL],
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data = {}
|
||||
if self.data and (scopes := self.data[CONF_SCOPE]) is not None:
|
||||
data[CONF_SCOPE] = " ".join(scopes)
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
|
||||
async def async_step_credentials_choice(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
@@ -5,4 +5,3 @@ DOMAIN = "mcp"
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
CONF_AUTHORIZATION_URL = "authorization_url"
|
||||
CONF_TOKEN_URL = "token_url"
|
||||
CONF_SCOPE = "scope"
|
||||
|
@@ -59,7 +59,7 @@ async def create_server(
|
||||
# Backwards compatibility with old MCP Server config
|
||||
return await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
|
||||
@server.list_prompts() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
@server.list_prompts() # type: ignore[no-untyped-call, misc]
|
||||
async def handle_list_prompts() -> list[types.Prompt]:
|
||||
llm_api = await get_api_instance()
|
||||
return [
|
||||
@@ -69,7 +69,7 @@ async def create_server(
|
||||
)
|
||||
]
|
||||
|
||||
@server.get_prompt() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
@server.get_prompt() # type: ignore[no-untyped-call, misc]
|
||||
async def handle_get_prompt(
|
||||
name: str, arguments: dict[str, str] | None
|
||||
) -> types.GetPromptResult:
|
||||
@@ -90,13 +90,13 @@ async def create_server(
|
||||
],
|
||||
)
|
||||
|
||||
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
@server.list_tools() # type: ignore[no-untyped-call, misc]
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
"""List available time tools."""
|
||||
llm_api = await get_api_instance()
|
||||
return [_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools]
|
||||
|
||||
@server.call_tool() # type: ignore[untyped-decorator]
|
||||
@server.call_tool() # type: ignore[misc]
|
||||
async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
|
||||
"""Handle calling tools."""
|
||||
llm_api = await get_api_instance()
|
||||
|
@@ -408,5 +408,5 @@ class AtwDeviceZoneClimate(MelCloudClimate):
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
await self._zone.set_target_temperature(
|
||||
kwargs.get(ATTR_TEMPERATURE, self.target_temperature)
|
||||
kwargs.get("temperature", self.target_temperature)
|
||||
)
|
||||
|
@@ -54,7 +54,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_PLATE_COUNT = 4
|
||||
|
||||
PLATE_COUNT = {
|
||||
"KM7575": 6,
|
||||
"KM7678": 6,
|
||||
"KM7697": 6,
|
||||
"KM7878": 6,
|
||||
|
@@ -10,11 +10,7 @@ from mill import Heater, Mill
|
||||
from mill_local import Mill as MillLocal
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import (
|
||||
StatisticData,
|
||||
StatisticMeanType,
|
||||
StatisticMetaData,
|
||||
)
|
||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
@@ -151,7 +147,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
)
|
||||
metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_mean=False,
|
||||
has_sum=True,
|
||||
name=f"{heater.name}",
|
||||
source=DOMAIN,
|
||||
|
@@ -253,7 +253,6 @@ class ModbusHub:
|
||||
self._client: (
|
||||
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
|
||||
) = None
|
||||
self._lock = asyncio.Lock()
|
||||
self.event_connected = asyncio.Event()
|
||||
self.hass = hass
|
||||
self.name = client_config[CONF_NAME]
|
||||
@@ -416,9 +415,7 @@ class ModbusHub:
|
||||
"""Convert async to sync pymodbus call."""
|
||||
if not self._client:
|
||||
return None
|
||||
async with self._lock:
|
||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||
if self._msg_wait:
|
||||
# small delay until next request/response
|
||||
await asyncio.sleep(self._msg_wait)
|
||||
return result
|
||||
|
@@ -174,7 +174,7 @@ class MotionBaseDevice(MotionCoordinatorEntity, CoverEntity):
|
||||
|
||||
_restore_tilt = False
|
||||
|
||||
def __init__(self, coordinator, blind, device_class) -> None:
|
||||
def __init__(self, coordinator, blind, device_class):
|
||||
"""Initialize the blind."""
|
||||
super().__init__(coordinator, blind)
|
||||
|
||||
@@ -275,7 +275,7 @@ class MotionTiltDevice(MotionPositionDevice):
|
||||
"""
|
||||
if self._blind.angle is None:
|
||||
return None
|
||||
return 100 - (self._blind.angle * 100 / 180)
|
||||
return self._blind.angle * 100 / 180
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
@@ -287,14 +287,14 @@ class MotionTiltDevice(MotionPositionDevice):
|
||||
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover tilt."""
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
@@ -302,7 +302,7 @@ class MotionTiltDevice(MotionPositionDevice):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, 180 - angle)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
@@ -347,9 +347,9 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
if self._blind.position is None:
|
||||
if self._blind.angle is None:
|
||||
return None
|
||||
return 100 - (self._blind.angle * 100 / 180)
|
||||
return self._blind.angle * 100 / 180
|
||||
|
||||
return 100 - self._blind.position
|
||||
return self._blind.position
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
@@ -357,9 +357,9 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
if self._blind.position is None:
|
||||
if self._blind.angle is None:
|
||||
return None
|
||||
return self._blind.angle == 180
|
||||
return self._blind.angle == 0
|
||||
|
||||
return self._blind.position == 100
|
||||
return self._blind.position == 0
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover tilt."""
|
||||
@@ -381,14 +381,10 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
if self._blind.position is None:
|
||||
angle = angle * 180 / 100
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._blind.Set_angle, 180 - angle
|
||||
)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
|
||||
else:
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._blind.Set_position, 100 - angle
|
||||
)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
@@ -401,14 +397,10 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
if self._blind.position is None:
|
||||
angle = angle * 180 / 100
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._blind.Set_angle, 180 - angle
|
||||
)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
|
||||
else:
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._blind.Set_position, 100 - angle
|
||||
)
|
||||
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
|
||||
|
||||
await self.async_request_position_till_stop()
|
||||
|
||||
@@ -416,7 +408,7 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
class MotionTDBUDevice(MotionBaseDevice):
|
||||
"""Representation of a Motion Top Down Bottom Up blind Device."""
|
||||
|
||||
def __init__(self, coordinator, blind, device_class, motor) -> None:
|
||||
def __init__(self, coordinator, blind, device_class, motor):
|
||||
"""Initialize the blind."""
|
||||
super().__init__(coordinator, blind, device_class)
|
||||
self._motor = motor
|
||||
|
@@ -458,6 +458,7 @@ SUBENTRY_PLATFORMS = [
|
||||
Platform.LOCK,
|
||||
Platform.NOTIFY,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
@@ -1141,6 +1142,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
|
||||
Platform.LOCK.value: None,
|
||||
Platform.NOTIFY.value: None,
|
||||
Platform.NUMBER.value: validate_number_platform_config,
|
||||
Platform.SELECT: None,
|
||||
Platform.SENSOR.value: validate_sensor_platform_config,
|
||||
Platform.SWITCH.value: None,
|
||||
}
|
||||
@@ -1367,6 +1369,7 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
|
||||
custom_filtering=True,
|
||||
),
|
||||
},
|
||||
Platform.SELECT.value: {},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_DEVICE_CLASS: PlatformField(
|
||||
selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False
|
||||
@@ -3103,6 +3106,34 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
|
||||
),
|
||||
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
|
||||
},
|
||||
Platform.SELECT.value: {
|
||||
CONF_COMMAND_TOPIC: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
required=True,
|
||||
validator=valid_publish_topic,
|
||||
error="invalid_publish_topic",
|
||||
),
|
||||
CONF_COMMAND_TEMPLATE: PlatformField(
|
||||
selector=TEMPLATE_SELECTOR,
|
||||
required=False,
|
||||
validator=validate(cv.template),
|
||||
error="invalid_template",
|
||||
),
|
||||
CONF_STATE_TOPIC: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
required=False,
|
||||
validator=valid_subscribe_topic,
|
||||
error="invalid_subscribe_topic",
|
||||
),
|
||||
CONF_VALUE_TEMPLATE: PlatformField(
|
||||
selector=TEMPLATE_SELECTOR,
|
||||
required=False,
|
||||
validator=validate(cv.template),
|
||||
error="invalid_template",
|
||||
),
|
||||
CONF_OPTIONS: PlatformField(selector=OPTIONS_SELECTOR, required=True),
|
||||
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
|
||||
},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_STATE_TOPIC: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
|
@@ -346,6 +346,7 @@
|
||||
"mode_state_template": "Operation mode value template",
|
||||
"on_command_type": "ON command type",
|
||||
"optimistic": "Optimistic",
|
||||
"options": "Set options",
|
||||
"payload_off": "Payload \"off\"",
|
||||
"payload_on": "Payload \"on\"",
|
||||
"payload_press": "Payload \"press\"",
|
||||
@@ -393,6 +394,7 @@
|
||||
"mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the operation mode state. [Learn more.]({url}#mode_state_template)",
|
||||
"on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.",
|
||||
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
|
||||
"options": "List of options that can be selected.",
|
||||
"payload_off": "The payload that represents the \"off\" state.",
|
||||
"payload_on": "The payload that represents the \"on\" state.",
|
||||
"payload_press": "The payload to send when the button is triggered.",
|
||||
@@ -1334,6 +1336,7 @@
|
||||
"lock": "[%key:component::lock::title%]",
|
||||
"notify": "[%key:component::notify::title%]",
|
||||
"number": "[%key:component::number::title%]",
|
||||
"select": "[%key:component::select::title%]",
|
||||
"sensor": "[%key:component::sensor::title%]",
|
||||
"switch": "[%key:component::switch::title%]"
|
||||
}
|
||||
|
@@ -53,7 +53,7 @@ class NikoHomeControlLight(NikoHomeControlEntity, LightEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
await self._action.turn_on(kwargs.get(ATTR_BRIGHTNESS))
|
||||
await self._action.turn_on(kwargs.get(ATTR_BRIGHTNESS, 255))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/niko_home_control",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["nikohomecontrol"],
|
||||
"requirements": ["nhc==0.6.1"]
|
||||
"requirements": ["nhc==0.4.12"]
|
||||
}
|
||||
|
@@ -1,51 +0,0 @@
|
||||
"""The Nintendo Switch Parental Controls integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pynintendoparental import Authenticator
|
||||
from pynintendoparental.exceptions import (
|
||||
InvalidOAuthConfigurationException,
|
||||
InvalidSessionTokenException,
|
||||
)
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SESSION_TOKEN, DOMAIN
|
||||
from .coordinator import NintendoParentalConfigEntry, NintendoUpdateCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: NintendoParentalConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Nintendo Switch Parental Controls from a config entry."""
|
||||
try:
|
||||
nintendo_auth = await Authenticator.complete_login(
|
||||
auth=None,
|
||||
response_token=entry.data[CONF_SESSION_TOKEN],
|
||||
is_session_token=True,
|
||||
client_session=async_get_clientsession(hass),
|
||||
)
|
||||
except (InvalidSessionTokenException, InvalidOAuthConfigurationException) as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_expired",
|
||||
) from err
|
||||
entry.runtime_data = coordinator = NintendoUpdateCoordinator(
|
||||
hass, nintendo_auth, entry
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: NintendoParentalConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
@@ -1,61 +0,0 @@
|
||||
"""Config flow for the Nintendo Switch Parental Controls integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pynintendoparental import Authenticator
|
||||
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SESSION_TOKEN, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Nintendo Switch Parental Controls."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a new config flow instance."""
|
||||
self.auth: Authenticator | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if self.auth is None:
|
||||
self.auth = Authenticator.generate_login(
|
||||
client_session=async_get_clientsession(self.hass)
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self.auth.complete_login(
|
||||
self.auth, user_input[CONF_API_TOKEN], False
|
||||
)
|
||||
except (ValueError, InvalidSessionTokenException, HttpException):
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
assert self.auth.account_id
|
||||
await self.async_set_unique_id(self.auth.account_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self.auth.account_id,
|
||||
data={
|
||||
CONF_SESSION_TOKEN: self.auth.get_session_token,
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
description_placeholders={"link": self.auth.login_url},
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
||||
errors=errors,
|
||||
)
|
@@ -1,5 +0,0 @@
|
||||
"""Constants for the Nintendo Switch Parental Controls integration."""
|
||||
|
||||
DOMAIN = "nintendo_parental"
|
||||
CONF_UPDATE_INTERVAL = "update_interval"
|
||||
CONF_SESSION_TOKEN = "session_token"
|
@@ -1,52 +0,0 @@
|
||||
"""Nintendo Parental Controls data coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pynintendoparental import Authenticator, NintendoParental
|
||||
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type NintendoParentalConfigEntry = ConfigEntry[NintendoUpdateCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Nintendo data update coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
authenticator: Authenticator,
|
||||
config_entry: NintendoParentalConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize update coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.api = NintendoParental(
|
||||
authenticator, hass.config.time_zone, hass.config.language
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data from Nintendo's API."""
|
||||
try:
|
||||
return await self.api.update()
|
||||
except InvalidOAuthConfigurationException as err:
|
||||
raise ConfigEntryError(
|
||||
err, translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
@@ -1,41 +0,0 @@
|
||||
"""Base entity definition for Nintendo Parental."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pynintendoparental.device import Device
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NintendoUpdateCoordinator
|
||||
|
||||
|
||||
class NintendoDevice(CoordinatorEntity[NintendoUpdateCoordinator]):
|
||||
"""Represent a Nintendo Switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: NintendoUpdateCoordinator, device: Device, key: str
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_unique_id = f"{device.device_id}_{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.device_id)},
|
||||
manufacturer="Nintendo",
|
||||
name=device.name,
|
||||
sw_version=device.extra["firmwareVersion"]["displayedVersion"],
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is loaded."""
|
||||
await super().async_added_to_hass()
|
||||
self._device.add_device_callback(self.async_write_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""When will be removed from HASS."""
|
||||
self._device.remove_device_callback(self.async_write_ha_state)
|
||||
await super().async_will_remove_from_hass()
|
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "nintendo_parental",
|
||||
"name": "Nintendo Switch Parental Controls",
|
||||
"codeowners": ["@pantherale0"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nintendo_parental",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoparental==1.1.1"]
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
No IP discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
No discovery.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
No specific icons defined.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
comment: |
|
||||
No issues in integration
|
||||
status: exempt
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
@@ -1,91 +0,0 @@
|
||||
"""Sensor platform for Nintendo Parental."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import NintendoParentalConfigEntry, NintendoUpdateCoordinator
|
||||
from .entity import Device, NintendoDevice
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class NintendoParentalSensor(StrEnum):
|
||||
"""Store keys for Nintendo Parental sensors."""
|
||||
|
||||
PLAYING_TIME = "playing_time"
|
||||
TIME_REMAINING = "time_remaining"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class NintendoParentalSensorEntityDescription(SensorEntityDescription):
|
||||
"""Description for Nintendo Parental sensor entities."""
|
||||
|
||||
value_fn: Callable[[Device], int | float | None]
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[NintendoParentalSensorEntityDescription, ...] = (
|
||||
NintendoParentalSensorEntityDescription(
|
||||
key=NintendoParentalSensor.PLAYING_TIME,
|
||||
translation_key=NintendoParentalSensor.PLAYING_TIME,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.today_playing_time,
|
||||
),
|
||||
NintendoParentalSensorEntityDescription(
|
||||
key=NintendoParentalSensor.TIME_REMAINING,
|
||||
translation_key=NintendoParentalSensor.TIME_REMAINING,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.today_time_remaining,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: NintendoParentalConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
async_add_devices(
|
||||
NintendoParentalSensorEntity(entry.runtime_data, device, sensor)
|
||||
for device in entry.runtime_data.api.devices.values()
|
||||
for sensor in SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class NintendoParentalSensorEntity(NintendoDevice, SensorEntity):
|
||||
"""Represent a single sensor."""
|
||||
|
||||
entity_description: NintendoParentalSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NintendoUpdateCoordinator,
|
||||
device: Device,
|
||||
description: NintendoParentalSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, device=device, key=description.key)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
"""Return the native value."""
|
||||
return self.entity_description.value_fn(self._device)
|
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To obtain your access token, click [Nintendo Login]({link}) to sign in to your Nintendo account. Then, for the account you want to link, right-click on the red **Select this person** button and choose **Copy Link Address**.",
|
||||
"data": {
|
||||
"api_token": "Access token"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The link copied from the Nintendo website"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"playing_time": {
|
||||
"name": "Used screen time"
|
||||
},
|
||||
"time_remaining": {
|
||||
"name": "Screen time remaining"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_expired": {
|
||||
"message": "Authentication expired. Please remove and re-add the integration to reconnect."
|
||||
}
|
||||
}
|
||||
}
|
@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
) from error
|
||||
except NordPoolEmptyResponseError:
|
||||
return {area: [] for area in areas}
|
||||
except (NordPoolError, TimeoutError) as error:
|
||||
except NordPoolError as error:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
|
@@ -22,7 +22,7 @@
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"auto": "Automatic",
|
||||
"box": "Input field",
|
||||
"box": "Box",
|
||||
"slider": "Slider"
|
||||
}
|
||||
},
|
||||
|
@@ -31,39 +31,38 @@ async def async_setup_entry(
|
||||
events = device.events.get_platform("binary_sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
uids = set()
|
||||
entities = []
|
||||
for event in events:
|
||||
uids.add(event.uid)
|
||||
entities.append(
|
||||
ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
|
||||
)
|
||||
entities = {
|
||||
event.uid: ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
|
||||
for event in events
|
||||
}
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id):
|
||||
if entry.domain == "binary_sensor" and entry.unique_id not in uids:
|
||||
uids.add(entry.unique_id)
|
||||
entities.append(ONVIFBinarySensor(entry.unique_id, device, entry=entry))
|
||||
if entry.domain == "binary_sensor" and entry.unique_id not in entities:
|
||||
entities[entry.unique_id] = ONVIFBinarySensor(
|
||||
entry.unique_id, device, entry=entry
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities.values())
|
||||
uids_by_platform = device.events.get_uids_by_platform("binary_sensor")
|
||||
|
||||
@callback
|
||||
def async_check_entities() -> None:
|
||||
"""Check if we have added an entity for the event."""
|
||||
nonlocal uids_by_platform
|
||||
if not (missing := uids_by_platform.difference(uids)):
|
||||
if not (missing := uids_by_platform.difference(entities)):
|
||||
return
|
||||
|
||||
events = device.events.get_platform("binary_sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
new_entities = [
|
||||
ONVIFBinarySensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
]
|
||||
new_entities: dict[str, ONVIFBinarySensor] = {
|
||||
uid: ONVIFBinarySensor(uid, device, name=entity_names[uid])
|
||||
for uid in missing
|
||||
}
|
||||
if new_entities:
|
||||
uids.update(missing)
|
||||
async_add_entities(new_entities)
|
||||
entities.update(new_entities)
|
||||
async_add_entities(new_entities.values())
|
||||
|
||||
device.events.async_add_listener(async_check_entities)
|
||||
|
||||
|
@@ -30,37 +30,37 @@ async def async_setup_entry(
|
||||
events = device.events.get_platform("sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
uids = set()
|
||||
entities = []
|
||||
for event in events:
|
||||
uids.add(event.uid)
|
||||
entities.append(ONVIFSensor(event.uid, device, name=entity_names[event.uid]))
|
||||
entities = {
|
||||
event.uid: ONVIFSensor(event.uid, device, name=entity_names[event.uid])
|
||||
for event in events
|
||||
}
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id):
|
||||
if entry.domain == "sensor" and entry.unique_id not in uids:
|
||||
uids.add(entry.unique_id)
|
||||
entities.append(ONVIFSensor(entry.unique_id, device, entry=entry))
|
||||
if entry.domain == "sensor" and entry.unique_id not in entities:
|
||||
entities[entry.unique_id] = ONVIFSensor(
|
||||
entry.unique_id, device, entry=entry
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities.values())
|
||||
uids_by_platform = device.events.get_uids_by_platform("sensor")
|
||||
|
||||
@callback
|
||||
def async_check_entities() -> None:
|
||||
"""Check if we have added an entity for the event."""
|
||||
nonlocal uids_by_platform
|
||||
if not (missing := uids_by_platform.difference(uids)):
|
||||
if not (missing := uids_by_platform.difference(entities)):
|
||||
return
|
||||
|
||||
events = device.events.get_platform("sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
new_entities = [
|
||||
ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
]
|
||||
new_entities: dict[str, ONVIFSensor] = {
|
||||
uid: ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
}
|
||||
if new_entities:
|
||||
uids.update(missing)
|
||||
async_add_entities(new_entities)
|
||||
entities.update(new_entities)
|
||||
async_add_entities(new_entities.values())
|
||||
|
||||
device.events.async_add_listener(async_check_entities)
|
||||
|
||||
|
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["openai==2.2.0", "python-open-router==0.3.1"]
|
||||
"requirements": ["openai==1.99.5", "python-open-router==0.3.1"]
|
||||
}
|
||||
|
@@ -316,23 +316,16 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
options = self.options
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
step_schema: VolDictType = {}
|
||||
|
||||
model = options[CONF_CHAT_MODEL]
|
||||
|
||||
if not model.startswith(("gpt-5-pro", "gpt-5-codex")):
|
||||
step_schema.update(
|
||||
{
|
||||
step_schema: VolDictType = {
|
||||
vol.Optional(
|
||||
CONF_CODE_INTERPRETER,
|
||||
default=RECOMMENDED_CODE_INTERPRETER,
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
elif CONF_CODE_INTERPRETER in options:
|
||||
options.pop(CONF_CODE_INTERPRETER)
|
||||
|
||||
if model.startswith(("o", "gpt-5")) and not model.startswith("gpt-5-pro"):
|
||||
model = options[CONF_CHAT_MODEL]
|
||||
|
||||
if model.startswith(("o", "gpt-5")):
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
|
@@ -468,9 +468,7 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
model_args["reasoning"] = {
|
||||
"effort": options.get(
|
||||
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
|
||||
)
|
||||
if not model_args["model"].startswith("gpt-5-pro")
|
||||
else "high", # GPT-5 pro only supports reasoning.effort: high
|
||||
),
|
||||
"summary": "auto",
|
||||
}
|
||||
model_args["include"] = ["reasoning.encrypted_content"]
|
||||
@@ -489,7 +487,7 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
|
||||
if options.get(CONF_WEB_SEARCH):
|
||||
web_search = WebSearchToolParam(
|
||||
type="web_search",
|
||||
type="web_search_preview",
|
||||
search_context_size=options.get(
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE
|
||||
),
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["openai==2.2.0"]
|
||||
"requirements": ["openai==1.99.5"]
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import logging
|
||||
from pyopenweathermap import create_owm_client
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS
|
||||
@@ -25,6 +25,7 @@ type OpenweathermapConfigEntry = ConfigEntry[OpenweathermapData]
|
||||
class OpenweathermapData:
|
||||
"""Runtime data definition."""
|
||||
|
||||
name: str
|
||||
mode: str
|
||||
coordinator: OWMUpdateCoordinator
|
||||
|
||||
@@ -33,6 +34,7 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: OpenweathermapConfigEntry
|
||||
) -> bool:
|
||||
"""Set up OpenWeatherMap as config entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
language = entry.options[CONF_LANGUAGE]
|
||||
mode = entry.options[CONF_MODE]
|
||||
@@ -49,7 +51,7 @@ async def async_setup_entry(
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
entry.runtime_data = OpenweathermapData(mode, owm_coordinator)
|
||||
entry.runtime_data = OpenweathermapData(name, mode, owm_coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@@ -14,17 +14,12 @@ from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import (
|
||||
LanguageSelector,
|
||||
LanguageSelectorConfig,
|
||||
LocationSelector,
|
||||
LocationSelectorConfig,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
CONFIG_FLOW_VERSION,
|
||||
@@ -39,12 +34,10 @@ from .utils import build_data_and_options, validate_api_key
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LOCATION): LocationSelector(
|
||||
LocationSelectorConfig(radius=False)
|
||||
),
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
|
||||
),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
|
||||
}
|
||||
@@ -52,9 +45,7 @@ USER_SCHEMA = vol.Schema(
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
|
||||
),
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
|
||||
}
|
||||
)
|
||||
@@ -79,8 +70,8 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LOCATION][CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LOCATION][CONF_LONGITUDE]
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
mode = user_input[CONF_MODE]
|
||||
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
@@ -91,21 +82,15 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
if not errors:
|
||||
# Flatten location
|
||||
location = user_input.pop(CONF_LOCATION)
|
||||
user_input[CONF_LATITUDE] = location[CONF_LATITUDE]
|
||||
user_input[CONF_LONGITUDE] = location[CONF_LONGITUDE]
|
||||
data, options = build_data_and_options(user_input)
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_NAME, data=data, options=options
|
||||
title=user_input[CONF_NAME], data=data, options=options
|
||||
)
|
||||
schema_data = user_input
|
||||
else:
|
||||
schema_data = {
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
},
|
||||
CONF_LANGUAGE: self.hass.config.language,
|
||||
}
|
||||
|
||||
|
@@ -121,7 +121,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CLOUDS,
|
||||
@@ -159,7 +158,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CONDITION,
|
||||
@@ -229,6 +227,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up OpenWeatherMap sensor entities based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
unique_id = config_entry.unique_id
|
||||
assert unique_id is not None
|
||||
coordinator = domain_data.coordinator
|
||||
@@ -243,6 +242,7 @@ async def async_setup_entry(
|
||||
elif domain_data.mode == OWM_MODE_AIRPOLLUTION:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
unique_id,
|
||||
description,
|
||||
coordinator,
|
||||
@@ -252,6 +252,7 @@ async def async_setup_entry(
|
||||
else:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
unique_id,
|
||||
description,
|
||||
coordinator,
|
||||
@@ -269,6 +270,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
description: SensorEntityDescription,
|
||||
coordinator: OWMUpdateCoordinator,
|
||||
@@ -282,6 +284,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -12,14 +12,16 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"language": "[%key:common::config_flow::data::language%]",
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "API key for the OpenWeatherMap integration",
|
||||
"language": "Language for the OpenWeatherMap content",
|
||||
"location": "Location to get the weather data for",
|
||||
"latitude": "Latitude of the location",
|
||||
"longitude": "Longitude of the location",
|
||||
"mode": "Mode for the OpenWeatherMap API",
|
||||
"name": "Name for this OpenWeatherMap location"
|
||||
},
|
||||
|
@@ -57,13 +57,14 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up OpenWeatherMap weather entity based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
mode = domain_data.mode
|
||||
|
||||
if mode != OWM_MODE_AIRPOLLUTION:
|
||||
weather_coordinator = domain_data.coordinator
|
||||
|
||||
unique_id = f"{config_entry.unique_id}"
|
||||
owm_weather = OpenWeatherMapWeather(unique_id, mode, weather_coordinator)
|
||||
owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator)
|
||||
|
||||
async_add_entities([owm_weather], False)
|
||||
|
||||
@@ -92,6 +93,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
mode: str,
|
||||
weather_coordinator: OWMUpdateCoordinator,
|
||||
@@ -103,6 +105,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=name,
|
||||
)
|
||||
self.mode = mode
|
||||
|
||||
|
@@ -18,8 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .coordinator import PortainerCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SWITCH]
|
||||
|
||||
type PortainerConfigEntry = ConfigEntry[PortainerCoordinator]
|
||||
|
||||
|
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"image": {
|
||||
"default": "mdi:docker"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"container": {
|
||||
"default": "mdi:arrow-down-box",
|
||||
|
@@ -1,83 +0,0 @@
|
||||
"""Sensor platform for Portainer integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyportainer.models.docker import DockerContainer
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import PortainerConfigEntry, PortainerCoordinator
|
||||
from .entity import PortainerContainerEntity, PortainerCoordinatorData
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PortainerSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class to hold Portainer sensor description."""
|
||||
|
||||
value_fn: Callable[[DockerContainer], str | None]
|
||||
|
||||
|
||||
CONTAINER_SENSORS: tuple[PortainerSensorEntityDescription, ...] = (
|
||||
PortainerSensorEntityDescription(
|
||||
key="image",
|
||||
translation_key="image",
|
||||
value_fn=lambda data: data.image,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: PortainerConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Portainer sensors based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PortainerContainerSensor(
|
||||
coordinator,
|
||||
entity_description,
|
||||
container,
|
||||
endpoint,
|
||||
)
|
||||
for endpoint in coordinator.data.values()
|
||||
for container in endpoint.containers.values()
|
||||
for entity_description in CONTAINER_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class PortainerContainerSensor(PortainerContainerEntity, SensorEntity):
|
||||
"""Representation of a Portainer container sensor."""
|
||||
|
||||
entity_description: PortainerSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerSensorEntityDescription,
|
||||
device_info: DockerContainer,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer container sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return super().available and self.endpoint_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self.endpoint_id].containers[self.device_id]
|
||||
)
|
@@ -46,11 +46,6 @@
|
||||
"name": "Status"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"image": {
|
||||
"name": "Image"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"container": {
|
||||
"name": "Container"
|
||||
|
@@ -48,7 +48,7 @@ from .const import (
|
||||
|
||||
DEFAULT_OFF_DELAY = 2.0
|
||||
|
||||
CONNECT_TIMEOUT = 60.0
|
||||
CONNECT_TIMEOUT = 30.0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -24,7 +24,6 @@
|
||||
},
|
||||
"config_subentries": {
|
||||
"partition": {
|
||||
"entry_type": "Partition",
|
||||
"initiate_flow": {
|
||||
"user": "Add partition"
|
||||
},
|
||||
@@ -58,7 +57,6 @@
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"entry_type": "Zone",
|
||||
"initiate_flow": {
|
||||
"user": "Add zone"
|
||||
},
|
||||
@@ -93,7 +91,6 @@
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"entry_type": "Output",
|
||||
"initiate_flow": {
|
||||
"user": "Add output"
|
||||
},
|
||||
@@ -128,7 +125,6 @@
|
||||
}
|
||||
},
|
||||
"switchable_output": {
|
||||
"entry_type": "Switchable output",
|
||||
"initiate_flow": {
|
||||
"user": "Add switchable output"
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "sharkiq",
|
||||
"name": "Shark IQ",
|
||||
"codeowners": ["@JeffResc", "@funkybunch", "@TheOneOgre"],
|
||||
"codeowners": ["@JeffResc", "@funkybunch"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||
"iot_class": "cloud_polling",
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import Final
|
||||
|
||||
from aioshelly.ble.const import BLE_SCRIPT_NAME
|
||||
@@ -64,7 +63,6 @@ from .repairs import (
|
||||
)
|
||||
from .utils import (
|
||||
async_create_issue_unsupported_firmware,
|
||||
async_migrate_rpc_virtual_components_unique_ids,
|
||||
get_coap_context,
|
||||
get_device_entry_gen,
|
||||
get_http_port,
|
||||
@@ -325,12 +323,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
translation_placeholders={"device": entry.title},
|
||||
) from err
|
||||
|
||||
await er.async_migrate_entries(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
partial(async_migrate_rpc_virtual_components_unique_ids, device.config),
|
||||
)
|
||||
|
||||
runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
|
||||
runtime_data.rpc.async_setup()
|
||||
runtime_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device)
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD, MODEL_FRANKEVER_WATER_VALVE
|
||||
from .const import CONF_SLEEP_PERIOD
|
||||
from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -157,18 +157,21 @@ SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
|
||||
key="input|input",
|
||||
name="Input",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_block_momentary_input,
|
||||
),
|
||||
("relay", "input"): BlockBinarySensorDescription(
|
||||
key="relay|input",
|
||||
name="Input",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_block_momentary_input,
|
||||
),
|
||||
("device", "input"): BlockBinarySensorDescription(
|
||||
key="device|input",
|
||||
name="Input",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_block_momentary_input,
|
||||
),
|
||||
("sensor", "extInput"): BlockBinarySensorDescription(
|
||||
@@ -198,6 +201,7 @@ RPC_SENSORS: Final = {
|
||||
key="input",
|
||||
sub_key="state",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_rpc_momentary_input,
|
||||
),
|
||||
"cloud": RpcBinarySensorDescription(
|
||||
@@ -266,21 +270,12 @@ RPC_SENSORS: Final = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"boolean_generic": RpcBinarySensorDescription(
|
||||
"boolean": RpcBinarySensorDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, BINARY_SENSOR_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
"boolean_has_power": RpcBinarySensorDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
role="has_power",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"calibration": RpcBinarySensorDescription(
|
||||
key="blutrv",
|
||||
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
from aioshelly.rpc_device import RpcDevice
|
||||
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN as BUTTON_PLATFORM,
|
||||
@@ -23,24 +24,16 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, LOGGER, MODEL_FRANKEVER_WATER_VALVE, SHELLY_GAS_MODELS
|
||||
from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
RpcEntityDescription,
|
||||
ShellyRpcAttributeEntity,
|
||||
async_setup_entry_rpc,
|
||||
get_entity_block_device_info,
|
||||
get_entity_rpc_device_info,
|
||||
rpc_call,
|
||||
)
|
||||
from .entity import get_entity_block_device_info, get_entity_rpc_device_info
|
||||
from .utils import (
|
||||
async_remove_orphaned_entities,
|
||||
format_ble_addr,
|
||||
get_blu_trv_device_info,
|
||||
get_device_entry_gen,
|
||||
get_rpc_entity_name,
|
||||
get_rpc_key_ids,
|
||||
get_rpc_key_instances,
|
||||
get_rpc_role_by_key,
|
||||
get_virtual_component_ids,
|
||||
)
|
||||
|
||||
@@ -58,11 +51,6 @@ class ShellyButtonDescription[
|
||||
supported: Callable[[_ShellyCoordinatorT], bool] = lambda _: True
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RpcButtonDescription(RpcEntityDescription, ButtonEntityDescription):
|
||||
"""Class to describe a RPC button."""
|
||||
|
||||
|
||||
BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
ShellyButtonDescription[ShellyBlockCoordinator | ShellyRpcCoordinator](
|
||||
key="reboot",
|
||||
@@ -108,24 +96,12 @@ BLU_TRV_BUTTONS: Final[list[ShellyButtonDescription]] = [
|
||||
),
|
||||
]
|
||||
|
||||
RPC_VIRTUAL_BUTTONS = {
|
||||
"button_generic": RpcButtonDescription(
|
||||
VIRTUAL_BUTTONS: Final[list[ShellyButtonDescription]] = [
|
||||
ShellyButtonDescription[ShellyRpcCoordinator](
|
||||
key="button",
|
||||
role="generic",
|
||||
),
|
||||
"button_open": RpcButtonDescription(
|
||||
key="button",
|
||||
entity_registry_enabled_default=False,
|
||||
role="open",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"button_close": RpcButtonDescription(
|
||||
key="button",
|
||||
entity_registry_enabled_default=False,
|
||||
role="close",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
}
|
||||
press_action="single_push",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@callback
|
||||
@@ -153,10 +129,8 @@ def async_migrate_unique_ids(
|
||||
)
|
||||
}
|
||||
|
||||
if not isinstance(coordinator, ShellyRpcCoordinator):
|
||||
return None
|
||||
|
||||
if blutrv_key_ids := get_rpc_key_ids(coordinator.device.status, BLU_TRV_IDENTIFIER):
|
||||
assert isinstance(coordinator.device, RpcDevice)
|
||||
for _id in blutrv_key_ids:
|
||||
key = f"{BLU_TRV_IDENTIFIER}:{_id}"
|
||||
ble_addr: str = coordinator.device.config[key]["addr"]
|
||||
@@ -175,26 +149,6 @@ def async_migrate_unique_ids(
|
||||
)
|
||||
}
|
||||
|
||||
if virtual_button_keys := get_rpc_key_instances(
|
||||
coordinator.device.config, "button"
|
||||
):
|
||||
for key in virtual_button_keys:
|
||||
old_unique_id = f"{coordinator.mac}-{key}"
|
||||
if entity_entry.unique_id == old_unique_id:
|
||||
role = get_rpc_role_by_key(coordinator.device.config, key)
|
||||
new_unique_id = f"{coordinator.mac}-{key}-button_{role}"
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -218,7 +172,7 @@ async def async_setup_entry(
|
||||
hass, config_entry.entry_id, partial(async_migrate_unique_ids, coordinator)
|
||||
)
|
||||
|
||||
entities: list[ShellyButton | ShellyBluTrvButton] = []
|
||||
entities: list[ShellyButton | ShellyBluTrvButton | ShellyVirtualButton] = []
|
||||
|
||||
entities.extend(
|
||||
ShellyButton(coordinator, button)
|
||||
@@ -231,8 +185,11 @@ async def async_setup_entry(
|
||||
return
|
||||
|
||||
# add virtual buttons
|
||||
async_setup_entry_rpc(
|
||||
hass, config_entry, async_add_entities, RPC_VIRTUAL_BUTTONS, RpcVirtualButton
|
||||
if virtual_button_ids := get_rpc_key_ids(coordinator.device.status, "button"):
|
||||
entities.extend(
|
||||
ShellyVirtualButton(coordinator, button, id_)
|
||||
for id_ in virtual_button_ids
|
||||
for button in VIRTUAL_BUTTONS
|
||||
)
|
||||
|
||||
# add BLU TRV buttons
|
||||
@@ -375,16 +332,30 @@ class ShellyBluTrvButton(ShellyBaseButton):
|
||||
await method(self._id)
|
||||
|
||||
|
||||
class RpcVirtualButton(ShellyRpcAttributeEntity, ButtonEntity):
|
||||
"""Defines a Shelly RPC virtual component button."""
|
||||
class ShellyVirtualButton(ShellyBaseButton):
|
||||
"""Defines a Shelly virtual component button."""
|
||||
|
||||
entity_description: RpcButtonDescription
|
||||
_id: int
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
description: ShellyButtonDescription,
|
||||
_id: int,
|
||||
) -> None:
|
||||
"""Initialize Shelly virtual component button."""
|
||||
super().__init__(coordinator, description)
|
||||
|
||||
@rpc_call
|
||||
async def async_press(self) -> None:
|
||||
"""Triggers the Shelly button press service."""
|
||||
self._attr_unique_id = f"{coordinator.mac}-{description.key}:{_id}"
|
||||
self._attr_device_info = get_entity_rpc_device_info(coordinator)
|
||||
self._attr_name = get_rpc_entity_name(
|
||||
coordinator.device, f"{description.key}:{_id}"
|
||||
)
|
||||
self._id = _id
|
||||
|
||||
async def _press_method(self) -> None:
|
||||
"""Press method."""
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self.coordinator, ShellyRpcCoordinator)
|
||||
|
||||
await self.coordinator.device.button_trigger(self._id, "single_push")
|
||||
await self.coordinator.device.button_trigger(
|
||||
self._id, self.entity_description.press_action
|
||||
)
|
||||
|
@@ -308,5 +308,3 @@ MODEL_NEO_WATER_VALVE = "NeoWaterValve"
|
||||
MODEL_FRANKEVER_WATER_VALVE = "WaterValve"
|
||||
MODEL_LINKEDGO_ST802_THERMOSTAT = "ST-802"
|
||||
MODEL_LINKEDGO_ST1820_THERMOSTAT = "ST1820"
|
||||
MODEL_TOP_EV_CHARGER_EVE01 = "EVE01"
|
||||
MODEL_FRANKEVER_IRRIGATION_CONTROLLER = "Irrigation"
|
||||
|
@@ -29,7 +29,6 @@ from .utils import (
|
||||
get_rpc_device_info,
|
||||
get_rpc_entity_name,
|
||||
get_rpc_key_instances,
|
||||
get_rpc_role_by_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -190,16 +189,14 @@ def async_setup_rpc_attribute_entities(
|
||||
if description.models and coordinator.model not in description.models:
|
||||
continue
|
||||
|
||||
if description.role and description.role != get_rpc_role_by_key(
|
||||
coordinator.device.config, key
|
||||
):
|
||||
if description.role and description.role != coordinator.device.config[
|
||||
key
|
||||
].get("role", "generic"):
|
||||
continue
|
||||
|
||||
if (
|
||||
description.sub_key
|
||||
and description.sub_key not in coordinator.device.status[key]
|
||||
and not description.supported(coordinator.device.status[key])
|
||||
):
|
||||
if description.sub_key not in coordinator.device.status[
|
||||
key
|
||||
] and not description.supported(coordinator.device.status[key]):
|
||||
continue
|
||||
|
||||
# Filter and remove entities that according to settings/status
|
||||
@@ -311,7 +308,7 @@ class RpcEntityDescription(EntityDescription):
|
||||
# restrict the type to str.
|
||||
name: str = ""
|
||||
|
||||
sub_key: str | None = None
|
||||
sub_key: str
|
||||
|
||||
value: Callable[[Any, Any], Any] | None = None
|
||||
available: Callable[[dict], bool] | None = None
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioshelly==13.12.0"],
|
||||
"requirements": ["aioshelly==13.11.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@@ -24,16 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
|
||||
from .const import (
|
||||
CONF_SLEEP_PERIOD,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
MODEL_FRANKEVER_WATER_VALVE,
|
||||
MODEL_LINKEDGO_ST802_THERMOSTAT,
|
||||
MODEL_LINKEDGO_ST1820_THERMOSTAT,
|
||||
MODEL_TOP_EV_CHARGER_EVE01,
|
||||
VIRTUAL_NUMBER_MODE_MAP,
|
||||
)
|
||||
from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER, VIRTUAL_NUMBER_MODE_MAP
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -192,7 +183,7 @@ RPC_NUMBERS: Final = {
|
||||
method="blu_trv_set_external_temperature",
|
||||
entity_class=RpcBluTrvExtTempNumber,
|
||||
),
|
||||
"number_generic": RpcNumberDescription(
|
||||
"number": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
@@ -206,58 +197,6 @@ RPC_NUMBERS: Final = {
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="generic",
|
||||
),
|
||||
"number_current_limit": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="current_limit",
|
||||
models={MODEL_TOP_EV_CHARGER_EVE01},
|
||||
),
|
||||
"number_position": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="position",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"number_target_humidity": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="target_humidity",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"number_target_temperature": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="target_temperature",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"valve_position": RpcNumberDescription(
|
||||
key="blutrv",
|
||||
|
@@ -38,13 +38,12 @@ class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
|
||||
|
||||
|
||||
RPC_SELECT_ENTITIES: Final = {
|
||||
"enum_generic": RpcSelectDescription(
|
||||
"enum": RpcSelectDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SELECT_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, cast
|
||||
from functools import partial
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
@@ -36,12 +37,13 @@ from homeassistant.const import (
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD
|
||||
from .const import CONF_SLEEP_PERIOD, LOGGER
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -549,7 +551,7 @@ RPC_SENSORS: Final = {
|
||||
"a_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="a_act_power",
|
||||
name="Power",
|
||||
name="Active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -559,7 +561,7 @@ RPC_SENSORS: Final = {
|
||||
"b_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="b_act_power",
|
||||
name="Power",
|
||||
name="Active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -569,7 +571,7 @@ RPC_SENSORS: Final = {
|
||||
"c_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="c_act_power",
|
||||
name="Power",
|
||||
name="Active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -579,7 +581,7 @@ RPC_SENSORS: Final = {
|
||||
"total_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_act_power",
|
||||
name="Power",
|
||||
name="Total active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -625,7 +627,7 @@ RPC_SENSORS: Final = {
|
||||
"total_aprt_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_aprt_power",
|
||||
name="Apparent power",
|
||||
name="Total apparent power",
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -880,7 +882,7 @@ RPC_SENSORS: Final = {
|
||||
"n_current": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="n_current",
|
||||
name="Neutral current",
|
||||
name="Phase N current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -890,7 +892,7 @@ RPC_SENSORS: Final = {
|
||||
"total_current": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_current",
|
||||
name="Current",
|
||||
name="Total current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -1382,7 +1384,7 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement="pulse",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value=lambda status, _: status["total"],
|
||||
removal_condition=lambda config, _, key: (
|
||||
removal_condition=lambda config, _status, key: (
|
||||
config[key]["type"] != "count" or config[key]["enable"] is False
|
||||
),
|
||||
),
|
||||
@@ -1422,7 +1424,7 @@ RPC_SENSORS: Final = {
|
||||
"text_generic": RpcSensorDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
@@ -1430,7 +1432,7 @@ RPC_SENSORS: Final = {
|
||||
"number_generic": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
unit=get_virtual_component_unit,
|
||||
@@ -1439,7 +1441,7 @@ RPC_SENSORS: Final = {
|
||||
"enum_generic": RpcSensorDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
options_fn=lambda config: config["options"],
|
||||
@@ -1454,7 +1456,7 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
removal_condition=lambda config, _, key: config[key].get("enable", False)
|
||||
removal_condition=lambda config, _status, key: config[key].get("enable", False)
|
||||
is False,
|
||||
entity_class=RpcBluTrvSensor,
|
||||
),
|
||||
@@ -1604,7 +1606,7 @@ RPC_SENSORS: Final = {
|
||||
"object_total_act_energy": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
name="Energy",
|
||||
name="Total Active Energy",
|
||||
value=lambda status, _: float(status["total_act_energy"]),
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1616,7 +1618,7 @@ RPC_SENSORS: Final = {
|
||||
"object_total_power": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
name="Power",
|
||||
name="Total Power",
|
||||
value=lambda status, _: float(status["total_power"]),
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
@@ -1661,6 +1663,39 @@ RPC_SENSORS: Final = {
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_unique_ids(
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate sensor unique IDs to include role."""
|
||||
if not entity_entry.entity_id.startswith("sensor."):
|
||||
return None
|
||||
|
||||
for sensor_id in ("text", "number", "enum"):
|
||||
old_unique_id = entity_entry.unique_id
|
||||
if old_unique_id.endswith(f"-{sensor_id}"):
|
||||
if entity_entry.original_device_class == SensorDeviceClass.HUMIDITY:
|
||||
new_unique_id = f"{old_unique_id}_current_humidity"
|
||||
elif entity_entry.original_device_class == SensorDeviceClass.TEMPERATURE:
|
||||
new_unique_id = f"{old_unique_id}_current_temperature"
|
||||
else:
|
||||
new_unique_id = f"{old_unique_id}_generic"
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ShellyConfigEntry,
|
||||
@@ -1680,6 +1715,12 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data.rpc
|
||||
assert coordinator
|
||||
|
||||
await er.async_migrate_entries(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
partial(async_migrate_unique_ids, coordinator),
|
||||
)
|
||||
|
||||
async_setup_entry_rpc(
|
||||
hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor
|
||||
)
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
@@ -21,13 +21,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import (
|
||||
MODEL_FRANKEVER_IRRIGATION_CONTROLLER,
|
||||
MODEL_LINKEDGO_ST802_THERMOSTAT,
|
||||
MODEL_LINKEDGO_ST1820_THERMOSTAT,
|
||||
MODEL_NEO_WATER_VALVE,
|
||||
MODEL_TOP_EV_CHARGER_EVE01,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -37,7 +30,6 @@ from .entity import (
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rpc,
|
||||
rpc_call,
|
||||
)
|
||||
from .utils import (
|
||||
async_remove_orphaned_entities,
|
||||
@@ -79,7 +71,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription):
|
||||
is_on: Callable[[dict[str, Any]], bool]
|
||||
method_on: str
|
||||
method_off: str
|
||||
method_params_fn: Callable[[int | None, bool], tuple]
|
||||
method_params_fn: Callable[[int | None, bool], dict]
|
||||
|
||||
|
||||
RPC_RELAY_SWITCHES = {
|
||||
@@ -88,145 +80,31 @@ RPC_RELAY_SWITCHES = {
|
||||
sub_key="output",
|
||||
removal_condition=is_rpc_exclude_from_relay,
|
||||
is_on=lambda status: bool(status["output"]),
|
||||
method_on="switch_set",
|
||||
method_off="switch_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
method_on="Switch.Set",
|
||||
method_off="Switch.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "on": value},
|
||||
),
|
||||
}
|
||||
|
||||
RPC_SWITCHES = {
|
||||
"boolean_generic": RpcSwitchDescription(
|
||||
"boolean": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SWITCH_PLATFORM
|
||||
),
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="generic",
|
||||
),
|
||||
"boolean_anti_freeze": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="anti_freeze",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"boolean_child_lock": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="child_lock",
|
||||
models={MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"boolean_enable": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="enable",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"boolean_start_charging": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="start_charging",
|
||||
models={MODEL_TOP_EV_CHARGER_EVE01},
|
||||
),
|
||||
"boolean_state": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="state",
|
||||
models={MODEL_NEO_WATER_VALVE},
|
||||
),
|
||||
"boolean_zone0": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="zone0",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone1": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="zone1",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone2": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="zone2",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone3": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="zone3",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone4": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="zone4",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone5": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="boolean_set",
|
||||
method_off="boolean_set",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
role="zone5",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
),
|
||||
"script": RpcSwitchDescription(
|
||||
key="script",
|
||||
sub_key="running",
|
||||
is_on=lambda status: bool(status["running"]),
|
||||
method_on="script_start",
|
||||
method_off="script_stop",
|
||||
method_params_fn=lambda id, _: (id,),
|
||||
method_on="Script.Start",
|
||||
method_off="Script.Stop",
|
||||
method_params_fn=lambda id, _: {"id": id},
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
@@ -423,27 +301,19 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity):
|
||||
"""If switch is on."""
|
||||
return self.entity_description.is_on(self.status)
|
||||
|
||||
@rpc_call
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on switch."""
|
||||
method = getattr(self.coordinator.device, self.entity_description.method_on)
|
||||
"""Turn on relay."""
|
||||
await self.call_rpc(
|
||||
self.entity_description.method_on,
|
||||
self.entity_description.method_params_fn(self._id, True),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
params = self.entity_description.method_params_fn(self._id, True)
|
||||
await method(*params)
|
||||
|
||||
@rpc_call
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off switch."""
|
||||
method = getattr(self.coordinator.device, self.entity_description.method_off)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
params = self.entity_description.method_params_fn(self._id, False)
|
||||
await method(*params)
|
||||
"""Turn off relay."""
|
||||
await self.call_rpc(
|
||||
self.entity_description.method_off,
|
||||
self.entity_description.method_params_fn(self._id, False),
|
||||
)
|
||||
|
||||
|
||||
class RpcRelaySwitch(RpcSwitch):
|
||||
|
@@ -38,13 +38,12 @@ class RpcTextDescription(RpcEntityDescription, TextEntityDescription):
|
||||
|
||||
|
||||
RPC_TEXT_ENTITIES: Final = {
|
||||
"text_generic": RpcTextDescription(
|
||||
"text": RpcTextDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, TEXT_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
}
|
||||
|
||||
|
@@ -484,11 +484,6 @@ def get_rpc_key_by_role(keys_dict: dict[str, Any], role: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_rpc_role_by_key(keys_dict: dict[str, Any], key: str) -> str:
|
||||
"""Return role by key for RPC device from a dict."""
|
||||
return cast(str, keys_dict[key].get("role", "generic"))
|
||||
|
||||
|
||||
def id_from_key(key: str) -> int:
|
||||
"""Return id from key."""
|
||||
return int(key.split(":")[-1])
|
||||
@@ -939,35 +934,3 @@ def remove_empty_sub_devices(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
def format_ble_addr(ble_addr: str) -> str:
|
||||
"""Format BLE address to use in unique_id."""
|
||||
return ble_addr.replace(":", "").upper()
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_rpc_virtual_components_unique_ids(
|
||||
config: dict[str, Any], entity_entry: er.RegistryEntry
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate RPC virtual components unique_ids to include role in the ID.
|
||||
|
||||
This is needed to support multiple components with the same key.
|
||||
The old unique_id format is: {mac}-{key}-{component}
|
||||
The new unique_id format is: {mac}-{key}-{component}_{role}
|
||||
"""
|
||||
for component in VIRTUAL_COMPONENTS:
|
||||
if entity_entry.unique_id.endswith(f"-{component!s}"):
|
||||
key = entity_entry.unique_id.split("-")[-2]
|
||||
if key not in config:
|
||||
continue
|
||||
role = get_rpc_role_by_key(config, key)
|
||||
new_unique_id = f"{entity_entry.unique_id}_{role}"
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
entity_entry.unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
entity_entry.unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
return None
|
||||
|
@@ -100,9 +100,8 @@ ATTR_PIN_VALUE = "pin"
|
||||
ATTR_TIMESTAMP = "timestamp"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
|
||||
WEBSOCKET_RECONNECT_RETRIES = 3
|
||||
WEBSOCKET_RETRY_DELAY = 2
|
||||
|
||||
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
|
||||
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
|
||||
@@ -420,7 +419,6 @@ class SimpliSafe:
|
||||
self._api = api
|
||||
self._hass = hass
|
||||
self._system_notifications: dict[int, set[SystemNotification]] = {}
|
||||
self._websocket_reconnect_retries: int = 0
|
||||
self._websocket_reconnect_task: asyncio.Task | None = None
|
||||
self.entry = entry
|
||||
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
|
||||
@@ -471,8 +469,6 @@ class SimpliSafe:
|
||||
"""Start a websocket reconnection loop."""
|
||||
assert self._api.websocket
|
||||
|
||||
self._websocket_reconnect_retries += 1
|
||||
|
||||
try:
|
||||
await self._api.websocket.async_connect()
|
||||
await self._api.websocket.async_listen()
|
||||
@@ -483,21 +479,9 @@ class SimpliSafe:
|
||||
LOGGER.error("Failed to connect to websocket: %s", err)
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
|
||||
else:
|
||||
self._websocket_reconnect_retries = 0
|
||||
|
||||
if self._websocket_reconnect_retries >= WEBSOCKET_RECONNECT_RETRIES:
|
||||
LOGGER.error("Max websocket connection retries exceeded")
|
||||
return
|
||||
|
||||
delay = WEBSOCKET_RETRY_DELAY * (2 ** (self._websocket_reconnect_retries - 1))
|
||||
LOGGER.info(
|
||||
"Retrying websocket connection in %s seconds (attempt %s/%s)",
|
||||
delay,
|
||||
self._websocket_reconnect_retries,
|
||||
WEBSOCKET_RECONNECT_RETRIES,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
LOGGER.debug("Reconnecting to websocket")
|
||||
await self._async_cancel_websocket_loop()
|
||||
self._websocket_reconnect_task = self._hass.async_create_task(
|
||||
self._async_start_websocket_loop()
|
||||
)
|
||||
|
@@ -179,13 +179,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
is_on_key="open",
|
||||
)
|
||||
},
|
||||
Capability.GAS_DETECTOR: {
|
||||
Attribute.GAS: SmartThingsBinarySensorEntityDescription(
|
||||
key=Attribute.GAS,
|
||||
device_class=BinarySensorDeviceClass.GAS,
|
||||
is_on_key="detected",
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@@ -34,17 +34,6 @@
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"wind_free": "mdi:weather-dust",
|
||||
"wind_free_sleep": "mdi:sleep",
|
||||
"quiet": "mdi:volume-off",
|
||||
"long_wind": "mdi:weather-windy",
|
||||
"smart": "mdi:leaf",
|
||||
"motion_direct": "mdi:account-arrow-left",
|
||||
"motion_indirect": "mdi:account-arrow-right"
|
||||
}
|
||||
},
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"turbo": "mdi:wind-power"
|
||||
|
@@ -530,6 +530,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.ILLUMINANCE_MEASUREMENT: {
|
||||
Attribute.ILLUMINANCE: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -841,6 +842,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
]
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.SIGNAL_STRENGTH: {
|
||||
Attribute.LQI: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -999,6 +1001,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.TVOC_MEASUREMENT: {
|
||||
Attribute.TVOC_LEVEL: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -1009,6 +1012,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
]
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.ULTRAVIOLET_INDEX: {
|
||||
Attribute.ULTRAVIOLET_INDEX: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
|
@@ -87,7 +87,7 @@
|
||||
"wind_free_sleep": "WindFree sleep",
|
||||
"quiet": "Quiet",
|
||||
"long_wind": "Long wind",
|
||||
"smart": "Smart saver",
|
||||
"smart": "Smart",
|
||||
"motion_direct": "Motion direct",
|
||||
"motion_indirect": "Motion indirect"
|
||||
}
|
||||
|
@@ -241,6 +241,7 @@ class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]):
|
||||
) -> StatisticMetaData:
|
||||
"""Build statistics metadata for requested configuration."""
|
||||
return StatisticMetaData(
|
||||
has_mean=False,
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"Suez water {name} {self._counter_id}",
|
||||
|
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["psutil"],
|
||||
"requirements": ["psutil-home-assistant==0.0.1", "psutil==7.1.0"],
|
||||
"requirements": ["psutil-home-assistant==0.0.1", "psutil==7.0.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -352,7 +352,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
{
|
||||
"code": self._set_temperature.dpcode,
|
||||
"value": round(
|
||||
self._set_temperature.scale_value_back(kwargs[ATTR_TEMPERATURE])
|
||||
self._set_temperature.scale_value_back(kwargs["temperature"])
|
||||
),
|
||||
}
|
||||
]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user