Compare commits

..

2 Commits

Author SHA1 Message Date
Paulus Schoutsen
fe35fac8ee Bad AI put a section back. 2025-10-03 09:48:43 -04:00
Paulus Schoutsen
4bccc57b46 Add config flow diagrams to Z-Wave JS docs 2025-10-03 09:31:45 -04:00
338 changed files with 4813 additions and 9283 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with: with:
category: "/language:python" category: "/language:python"

View File

@@ -17,7 +17,7 @@ jobs:
# - No PRs marked as no-stale # - No PRs marked as no-stale
# - No issues (-1) # - No issues (-1)
- name: 60 days stale PRs policy - name: 60 days stale PRs policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60 days-before-stale: 60
@@ -57,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted # - No issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: 90 days stale issues - name: 90 days stale issues
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
with: with:
repo-token: ${{ steps.token.outputs.token }} repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90 days-before-stale: 90
@@ -87,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted # - No Issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: Needs more information stale issues policy - name: Needs more information stale issues policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
with: with:
repo-token: ${{ steps.token.outputs.token }} repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information" only-labels: "needs-more-information"

View File

@@ -555,7 +555,6 @@ homeassistant.components.vacuum.*
homeassistant.components.vallox.* homeassistant.components.vallox.*
homeassistant.components.valve.* homeassistant.components.valve.*
homeassistant.components.velbus.* homeassistant.components.velbus.*
homeassistant.components.vivotek.*
homeassistant.components.vlc_telnet.* homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.* homeassistant.components.vodafone_station.*
homeassistant.components.volvo.* homeassistant.components.volvo.*

View File

@@ -1 +0,0 @@
.github/copilot-instructions.md

4
CODEOWNERS generated
View File

@@ -1065,8 +1065,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/nilu/ @hfurubotten /homeassistant/components/nilu/ @hfurubotten
/homeassistant/components/nina/ @DeerMaximum /homeassistant/components/nina/ @DeerMaximum
/tests/components/nina/ @DeerMaximum /tests/components/nina/ @DeerMaximum
/homeassistant/components/nintendo_parental/ @pantherale0
/tests/components/nintendo_parental/ @pantherale0
/homeassistant/components/nissan_leaf/ @filcole /homeassistant/components/nissan_leaf/ @filcole
/homeassistant/components/noaa_tides/ @jdelaney72 /homeassistant/components/noaa_tides/ @jdelaney72
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe /homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
@@ -1198,6 +1196,8 @@ build.json @home-assistant/supervisor
/tests/components/plex/ @jjlawren /tests/components/plex/ @jjlawren
/homeassistant/components/plugwise/ @CoMPaTech @bouwew /homeassistant/components/plugwise/ @CoMPaTech @bouwew
/tests/components/plugwise/ @CoMPaTech @bouwew /tests/components/plugwise/ @CoMPaTech @bouwew
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
/homeassistant/components/point/ @fredrike /homeassistant/components/point/ @fredrike
/tests/components/point/ @fredrike /tests/components/point/ @fredrike
/homeassistant/components/pooldose/ @lmaertin /homeassistant/components/pooldose/ @lmaertin

View File

@@ -635,15 +635,25 @@ async def async_enable_logging(
err_log_path = os.path.abspath(log_file) err_log_path = os.path.abspath(log_file)
if err_log_path: if err_log_path:
err_handler = await hass.async_add_executor_job( err_path_exists = os.path.isfile(err_log_path)
_create_log_file, err_log_path, log_rotate_days err_dir = os.path.dirname(err_log_path)
)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) # Check if we can write to the error log if it exists or that
logger.addHandler(err_handler) # we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
# Save the log file location for access by other components. err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
hass.data[DATA_LOGGING] = err_log_path logger.addHandler(err_handler)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
async_activate_log_queue_handler(hass) async_activate_log_queue_handler(hass)

View File

@@ -0,0 +1,5 @@
{
"domain": "ibm",
"name": "IBM",
"integrations": ["watson_iot", "watson_tts"]
}

View File

@@ -12,13 +12,11 @@ from homeassistant.components.bluetooth import async_get_scanner
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_IS_NEW_STYLE_SCALE from .const import CONF_IS_NEW_STYLE_SCALE
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
UPDATE_DEBOUNCE_TIME = 0.2
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -40,19 +38,11 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
config_entry=entry, config_entry=entry,
) )
debouncer = Debouncer(
hass=hass,
logger=_LOGGER,
cooldown=UPDATE_DEBOUNCE_TIME,
immediate=True,
function=self.async_update_listeners,
)
self._scale = AcaiaScale( self._scale = AcaiaScale(
address_or_ble_device=entry.data[CONF_ADDRESS], address_or_ble_device=entry.data[CONF_ADDRESS],
name=entry.title, name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE], is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=debouncer.async_schedule_call, notify_callback=self.async_update_listeners,
scanner=async_get_scanner(hass), scanner=async_get_scanner(hass),
) )

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos", "documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["airos==0.5.5"] "requirements": ["airos==0.5.4"]
} }

View File

@@ -6,13 +6,8 @@ import dataclasses
import logging import logging
from typing import Any from typing import Any
from airthings_ble import ( from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
AirthingsBluetoothDeviceData,
AirthingsDevice,
UnsupportedDeviceError,
)
from bleak import BleakError from bleak import BleakError
from habluetooth import BluetoothServiceInfoBleak
import voluptuous as vol import voluptuous as vol
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
@@ -32,7 +27,6 @@ SERVICE_UUIDS = [
"b42e4a8e-ade7-11e4-89d3-123b93f75cba", "b42e4a8e-ade7-11e4-89d3-123b93f75cba",
"b42e1c08-ade7-11e4-89d3-123b93f75cba", "b42e1c08-ade7-11e4-89d3-123b93f75cba",
"b42e3882-ade7-11e4-89d3-123b93f75cba", "b42e3882-ade7-11e4-89d3-123b93f75cba",
"b42e90a2-ade7-11e4-89d3-123b93f75cba",
] ]
@@ -43,7 +37,6 @@ class Discovery:
name: str name: str
discovery_info: BluetoothServiceInfo discovery_info: BluetoothServiceInfo
device: AirthingsDevice device: AirthingsDevice
data: AirthingsBluetoothDeviceData
def get_name(device: AirthingsDevice) -> str: def get_name(device: AirthingsDevice) -> str:
@@ -51,7 +44,7 @@ def get_name(device: AirthingsDevice) -> str:
name = device.friendly_name() name = device.friendly_name()
if identifier := device.identifier: if identifier := device.identifier:
name += f" ({device.model.value}{identifier})" name += f" ({identifier})"
return name return name
@@ -69,8 +62,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered_device: Discovery | None = None self._discovered_device: Discovery | None = None
self._discovered_devices: dict[str, Discovery] = {} self._discovered_devices: dict[str, Discovery] = {}
async def _get_device( async def _get_device_data(
self, data: AirthingsBluetoothDeviceData, discovery_info: BluetoothServiceInfo self, discovery_info: BluetoothServiceInfo
) -> AirthingsDevice: ) -> AirthingsDevice:
ble_device = bluetooth.async_ble_device_from_address( ble_device = bluetooth.async_ble_device_from_address(
self.hass, discovery_info.address self.hass, discovery_info.address
@@ -79,8 +72,10 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("no ble_device in _get_device_data") _LOGGER.debug("no ble_device in _get_device_data")
raise AirthingsDeviceUpdateError("No ble_device") raise AirthingsDeviceUpdateError("No ble_device")
airthings = AirthingsBluetoothDeviceData(_LOGGER)
try: try:
device = await data.update_device(ble_device) data = await airthings.update_device(ble_device)
except BleakError as err: except BleakError as err:
_LOGGER.error( _LOGGER.error(
"Error connecting to and getting data from %s: %s", "Error connecting to and getting data from %s: %s",
@@ -88,15 +83,12 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
err, err,
) )
raise AirthingsDeviceUpdateError("Failed getting device data") from err raise AirthingsDeviceUpdateError("Failed getting device data") from err
except UnsupportedDeviceError:
_LOGGER.debug("Skipping unsupported device: %s", discovery_info.name)
raise
except Exception as err: except Exception as err:
_LOGGER.error( _LOGGER.error(
"Unknown error occurred from %s: %s", discovery_info.address, err "Unknown error occurred from %s: %s", discovery_info.address, err
) )
raise raise
return device return data
async def async_step_bluetooth( async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo self, discovery_info: BluetoothServiceInfo
@@ -106,21 +98,17 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery_info.address) await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try: try:
device = await self._get_device(data=data, discovery_info=discovery_info) device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError: except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except UnsupportedDeviceError:
return self.async_abort(reason="unsupported_device")
except Exception: except Exception:
_LOGGER.exception("Unknown error occurred") _LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = get_name(device) name = get_name(device)
self.context["title_placeholders"] = {"name": name} self.context["title_placeholders"] = {"name": name}
self._discovered_device = Discovery(name, discovery_info, device, data=data) self._discovered_device = Discovery(name, discovery_info, device)
return await self.async_step_bluetooth_confirm() return await self.async_step_bluetooth_confirm()
@@ -129,12 +117,6 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Confirm discovery.""" """Confirm discovery."""
if user_input is not None: if user_input is not None:
if (
self._discovered_device is not None
and self._discovered_device.device.firmware.need_firmware_upgrade
):
return self.async_abort(reason="firmware_upgrade_required")
return self.async_create_entry( return self.async_create_entry(
title=self.context["title_placeholders"]["name"], data={} title=self.context["title_placeholders"]["name"], data={}
) )
@@ -155,9 +137,6 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
discovery = self._discovered_devices[address] discovery = self._discovered_devices[address]
if discovery.device.firmware.need_firmware_upgrade:
return self.async_abort(reason="firmware_upgrade_required")
self.context["title_placeholders"] = { self.context["title_placeholders"] = {
"name": discovery.name, "name": discovery.name,
} }
@@ -167,47 +146,26 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=discovery.name, data={}) return self.async_create_entry(title=discovery.name, data={})
current_addresses = self._async_current_ids(include_ignore=False) current_addresses = self._async_current_ids(include_ignore=False)
devices: list[BluetoothServiceInfoBleak] = []
for discovery_info in async_discovered_service_info(self.hass): for discovery_info in async_discovered_service_info(self.hass):
address = discovery_info.address address = discovery_info.address
if address in current_addresses or address in self._discovered_devices: if address in current_addresses or address in self._discovered_devices:
continue continue
if MFCT_ID not in discovery_info.manufacturer_data: if MFCT_ID not in discovery_info.manufacturer_data:
continue continue
if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
_LOGGER.debug(
"Skipping unsupported device: %s (%s)", discovery_info.name, address
)
continue
devices.append(discovery_info)
for discovery_info in devices: if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
address = discovery_info.address continue
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try: try:
device = await self._get_device(data, discovery_info) device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError: except AirthingsDeviceUpdateError:
_LOGGER.error( return self.async_abort(reason="cannot_connect")
"Error connecting to and getting data from %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except UnsupportedDeviceError:
_LOGGER.debug(
"Skipping unsupported device: %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except Exception: except Exception:
_LOGGER.exception("Unknown error occurred") _LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = get_name(device) name = get_name(device)
_LOGGER.debug("Discovered Airthings device: %s (%s)", name, address) self._discovered_devices[address] = Discovery(name, discovery_info, device)
self._discovered_devices[address] = Discovery(
name, discovery_info, device, data
)
if not self._discovered_devices: if not self._discovered_devices:
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")

View File

@@ -17,10 +17,6 @@
{ {
"manufacturer_id": 820, "manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba" "service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba"
} }
], ],
"codeowners": ["@vincegio", "@LaStrada"], "codeowners": ["@vincegio", "@LaStrada"],
@@ -28,5 +24,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airthings-ble==1.1.1"] "requirements": ["airthings-ble==0.9.2"]
} }

View File

@@ -20,8 +20,6 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
"unsupported_device": "Unsupported device",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
} }
}, },

View File

@@ -18,9 +18,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.helpers.entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonConfigEntry from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity from .entity import AmazonEntity
from .utils import async_update_unique_id from .utils import async_update_unique_id
@@ -53,47 +51,11 @@ BINARY_SENSORS: Final = (
), ),
is_supported=lambda device, key: device.sensors.get(key) is not None, is_supported=lambda device, key: device.sensors.get(key) is not None,
is_available_fn=lambda device, key: ( is_available_fn=lambda device, key: (
device.online device.online and device.sensors[key].error is False
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
), ),
), ),
) )
DEPRECATED_BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="babyCryDetectionState",
translation_key="baby_cry_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="beepingApplianceDetectionState",
translation_key="beeping_appliance_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="coughDetectionState",
translation_key="cough_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="dogBarkDetectionState",
translation_key="dog_bark_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="waterSoundsDetectionState",
translation_key="water_sounds_detection",
is_on_fn=lambda device, key: False,
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -104,8 +66,6 @@ async def async_setup_entry(
coordinator = entry.runtime_data coordinator = entry.runtime_data
entity_registry = er.async_get(hass)
# Replace unique id for "detectionState" binary sensor # Replace unique id for "detectionState" binary sensor
await async_update_unique_id( await async_update_unique_id(
hass, hass,
@@ -115,16 +75,6 @@ async def async_setup_entry(
"detectionState", "detectionState",
) )
# Clean up deprecated sensors
for sensor_desc in DEPRECATED_BINARY_SENSORS:
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{sensor_desc.key}"
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
):
_LOGGER.debug("Removing deprecated entity %s", entity_id)
entity_registry.async_remove(entity_id)
known_devices: set[str] = set() known_devices: set[str] = set()
def _check_device() -> None: def _check_device() -> None:

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioamazondevices"], "loggers": ["aioamazondevices"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aioamazondevices==6.2.9"] "requirements": ["aioamazondevices==6.2.7"]
} }

View File

@@ -32,9 +32,7 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online device.online and device.sensors[key].error is False
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
) )
@@ -42,9 +40,9 @@ SENSORS: Final = (
AmazonSensorEntityDescription( AmazonSensorEntityDescription(
key="temperature", key="temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device, key: ( native_unit_of_measurement_fn=lambda device, _key: (
UnitOfTemperature.CELSIUS UnitOfTemperature.CELSIUS
if key in device.sensors and device.sensors[key].scale == "CELSIUS" if device.sensors[_key].scale == "CELSIUS"
else UnitOfTemperature.FAHRENHEIT else UnitOfTemperature.FAHRENHEIT
), ),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,

View File

@@ -18,11 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity from .entity import AmazonEntity
from .utils import ( from .utils import alexa_api_call, async_update_unique_id
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@@ -33,9 +29,7 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
is_on_fn: Callable[[AmazonDevice], bool] is_on_fn: Callable[[AmazonDevice], bool]
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online device.online and device.sensors[key].error is False
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
) )
method: str method: str
@@ -64,9 +58,6 @@ async def async_setup_entry(
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd" hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
) )
# Remove DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator)
known_devices: set[str] = set() known_devices: set[str] = set()
def _check_device() -> None: def _check_device() -> None:

View File

@@ -4,10 +4,8 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps from functools import wraps
from typing import Any, Concatenate from typing import Any, Concatenate
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
@@ -63,21 +61,3 @@ async def async_update_unique_id(
# Update the registry with the new unique_id # Update the registry with the new unique_id
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_remove_dnd_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
) -> None:
"""Remove entity DND from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-do_not_disturb"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)

View File

@@ -65,31 +65,6 @@ SENSOR_DESCRIPTIONS = [
suggested_display_precision=2, suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME280"}, translation_placeholders={"sensor_name": "BME280"},
), ),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key="BME680_humidity",
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BME680_pressure",
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.PA,
suggested_unit_of_measurement=UnitOfPressure.MMHG,
suggested_display_precision=0,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="BME680_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription( AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
key="BMP_pressure", key="BMP_pressure",

View File

@@ -19,8 +19,9 @@ CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0 RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024 MIN_THINKING_BUDGET = 1024
NON_THINKING_MODELS = [ THINKING_MODELS = [
"claude-3-5", # Both sonnet and haiku "claude-3-7-sonnet",
"claude-3-opus", "claude-sonnet-4-0",
"claude-3-haiku", "claude-opus-4-0",
"claude-opus-4-1",
] ]

View File

@@ -51,11 +51,11 @@ from .const import (
DOMAIN, DOMAIN,
LOGGER, LOGGER,
MIN_THINKING_BUDGET, MIN_THINKING_BUDGET,
NON_THINKING_MODELS,
RECOMMENDED_CHAT_MODEL, RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS, RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE, RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
) )
# Max number of back and forth with the LLM to generate a response # Max number of back and forth with the LLM to generate a response
@@ -364,7 +364,7 @@ class AnthropicBaseLLMEntity(Entity):
if tools: if tools:
model_args["tools"] = tools model_args["tools"] = tools
if ( if (
not model.startswith(tuple(NON_THINKING_MODELS)) model.startswith(tuple(THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET and thinking_budget >= MIN_THINKING_BUDGET
): ):
model_args["thinking"] = ThinkingConfigEnabledParam( model_args["thinking"] = ThinkingConfigEnabledParam(

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic", "documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["anthropic==0.69.0"] "requirements": ["anthropic==0.62.0"]
} }

View File

@@ -19,8 +19,8 @@
"bleak-retry-connector==4.4.3", "bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0", "bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3", "bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.3", "bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.5", "dbus-fast==2.44.3",
"habluetooth==5.7.0" "habluetooth==5.6.4"
] ]
} }

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from asyncio.exceptions import TimeoutError from asyncio.exceptions import TimeoutError
from collections.abc import Mapping from collections.abc import Mapping
import re
from typing import Any from typing import Any
from aiocomelit import ( from aiocomelit import (
@@ -28,20 +27,25 @@ from .utils import async_client_session
DEFAULT_HOST = "192.168.1.252" DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = "111111" DEFAULT_PIN = "111111"
pin_regex = r"^[0-9]{4,10}$"
USER_SCHEMA = vol.Schema( USER_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string, vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST), vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
} }
) )
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string}) STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
)
STEP_RECONFIGURE = vol.Schema( STEP_RECONFIGURE = vol.Schema(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port, vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string, vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
} }
) )
@@ -51,9 +55,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
api: ComelitCommonApi api: ComelitCommonApi
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
raise InvalidPin
session = await async_client_session(hass) session = await async_client_session(hass)
if data.get(CONF_TYPE, BRIDGE) == BRIDGE: if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
api = ComeliteSerialBridgeApi( api = ComeliteSerialBridgeApi(
@@ -104,8 +105,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -147,8 +146,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -192,8 +189,6 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -215,7 +210,3 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError): class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth.""" """Error to indicate there is invalid auth."""
class InvalidPin(HomeAssistantError):
"""Error to indicate an invalid pin."""

View File

@@ -161,7 +161,7 @@ class ComelitSerialBridge(
entry: ComelitConfigEntry, entry: ComelitConfigEntry,
host: str, host: str,
port: int, port: int,
pin: str, pin: int,
session: ClientSession, session: ClientSession,
) -> None: ) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
entry: ComelitConfigEntry, entry: ComelitConfigEntry,
host: str, host: str,
port: int, port: int,
pin: str, pin: int,
session: ClientSession, session: ClientSession,
) -> None: ) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiocomelit"], "loggers": ["aiocomelit"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiocomelit==1.1.1"] "requirements": ["aiocomelit==0.12.3"]
} }

View File

@@ -43,13 +43,11 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
} }
}, },

View File

@@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util.ssl import client_context_no_verify from homeassistant.util.ssl import client_context_no_verify
from .const import KEY_MAC, TIMEOUT_SEC from .const import KEY_MAC, TIMEOUT
from .coordinator import DaikinConfigEntry, DaikinCoordinator from .coordinator import DaikinConfigEntry, DaikinCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
host = conf[CONF_HOST] host = conf[CONF_HOST]
try: try:
async with asyncio.timeout(TIMEOUT_SEC): async with asyncio.timeout(TIMEOUT):
device: Appliance = await DaikinFactory( device: Appliance = await DaikinFactory(
host, host,
session, session,
@@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
) )
_LOGGER.debug("Connection to %s successful", host) _LOGGER.debug("Connection to %s successful", host)
except TimeoutError as err: except TimeoutError as err:
_LOGGER.debug("Connection to %s timed out in %s seconds", host, TIMEOUT_SEC) _LOGGER.debug("Connection to %s timed out in 60 seconds", host)
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
except ClientConnectionError as err: except ClientConnectionError as err:
_LOGGER.debug("ClientConnectionError to %s", host) _LOGGER.debug("ClientConnectionError to %s", host)

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.util.ssl import client_context_no_verify from homeassistant.util.ssl import client_context_no_verify
from .const import DOMAIN, KEY_MAC, TIMEOUT_SEC from .const import DOMAIN, KEY_MAC, TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -84,7 +84,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
password = None password = None
try: try:
async with asyncio.timeout(TIMEOUT_SEC): async with asyncio.timeout(TIMEOUT):
device: Appliance = await DaikinFactory( device: Appliance = await DaikinFactory(
host, host,
async_get_clientsession(self.hass), async_get_clientsession(self.hass),

View File

@@ -24,4 +24,4 @@ ATTR_STATE_OFF = "off"
KEY_MAC = "mac" KEY_MAC = "mac"
KEY_IP = "ip" KEY_IP = "ip"
TIMEOUT_SEC = 120 TIMEOUT = 60

View File

@@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, TIMEOUT_SEC from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ class DaikinCoordinator(DataUpdateCoordinator[None]):
_LOGGER, _LOGGER,
config_entry=entry, config_entry=entry,
name=device.values.get("name", DOMAIN), name=device.values.get("name", DOMAIN),
update_interval=timedelta(seconds=TIMEOUT_SEC), update_interval=timedelta(seconds=60),
) )
self.device = device self.device = device

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin", "documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pydaikin"], "loggers": ["pydaikin"],
"requirements": ["pydaikin==2.17.1"], "requirements": ["pydaikin==2.16.0"],
"zeroconf": ["_dkapi._tcp.local."] "zeroconf": ["_dkapi._tcp.local."]
} }

View File

@@ -17,6 +17,6 @@
"requirements": [ "requirements": [
"aiodhcpwatcher==1.2.1", "aiodhcpwatcher==1.2.1",
"aiodiscover==2.7.1", "aiodiscover==2.7.1",
"cached-ipaddress==1.0.1" "cached-ipaddress==0.10.0"
] ]
} }

View File

@@ -116,9 +116,6 @@
} }
}, },
"select": { "select": {
"active_map": {
"default": "mdi:floor-plan"
},
"water_amount": { "water_amount": {
"default": "mdi:water" "default": "mdi:water"
}, },

View File

@@ -2,13 +2,12 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any from typing import Any
from deebot_client.capabilities import CapabilityMap, CapabilitySet, CapabilitySetTypes from deebot_client.capabilities import CapabilitySetTypes
from deebot_client.device import Device from deebot_client.device import Device
from deebot_client.events import WorkModeEvent from deebot_client.events import WorkModeEvent
from deebot_client.events.base import Event from deebot_client.events.base import Event
from deebot_client.events.map import CachedMapInfoEvent, MajorMapEvent
from deebot_client.events.water_info import WaterAmountEvent from deebot_client.events.water_info import WaterAmountEvent
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -17,11 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry from . import EcovacsConfigEntry
from .entity import ( from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity,
EcovacsEntity,
)
from .util import get_name_key, get_supported_entities from .util import get_name_key, get_supported_entities
@@ -71,12 +66,6 @@ async def async_setup_entry(
entities = get_supported_entities( entities = get_supported_entities(
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
) )
entities.extend(
EcovacsActiveMapSelectEntity(device, device.capabilities.map)
for device in controller.devices
if (map_cap := device.capabilities.map)
and isinstance(map_cap.major, CapabilitySet)
)
if entities: if entities:
async_add_entities(entities) async_add_entities(entities)
@@ -114,76 +103,3 @@ class EcovacsSelectEntity[EventT: Event](
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
await self._device.execute_command(self._capability.set(option)) await self._device.execute_command(self._capability.set(option))
class EcovacsActiveMapSelectEntity(
EcovacsEntity[CapabilityMap],
SelectEntity,
):
"""Ecovacs active map select entity."""
entity_description = SelectEntityDescription(
key="active_map",
translation_key="active_map",
entity_category=EntityCategory.CONFIG,
)
def __init__(
self,
device: Device,
capability: CapabilityMap,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, **kwargs)
self._option_to_id: dict[str, str] = {}
self._id_to_option: dict[str, str] = {}
self._handle_on_cached_map(
device.events.get_last_event(CachedMapInfoEvent)
or CachedMapInfoEvent(set())
)
def _handle_on_cached_map(self, event: CachedMapInfoEvent) -> None:
self._id_to_option.clear()
self._option_to_id.clear()
for map_info in event.maps:
name = map_info.name if map_info.name else map_info.id
self._id_to_option[map_info.id] = name
self._option_to_id[name] = map_info.id
if map_info.using:
self._attr_current_option = name
if self._attr_current_option not in self._option_to_id:
self._attr_current_option = None
# Sort named maps first, then numeric IDs (unnamed maps during building) in ascending order.
self._attr_options = sorted(
self._option_to_id.keys(), key=lambda x: (x.isdigit(), x.lower())
)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
async def on_cached_map(event: CachedMapInfoEvent) -> None:
self._handle_on_cached_map(event)
self.async_write_ha_state()
self._subscribe(self._capability.cached_info.event, on_cached_map)
async def on_major_map(event: MajorMapEvent) -> None:
self._attr_current_option = self._id_to_option.get(event.map_id)
self.async_write_ha_state()
self._subscribe(self._capability.major.event, on_major_map)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
if TYPE_CHECKING:
assert isinstance(self._capability.major, CapabilitySet)
await self._device.execute_command(
self._capability.major.set(self._option_to_id[option])
)

View File

@@ -178,9 +178,6 @@
} }
}, },
"select": { "select": {
"active_map": {
"name": "Active map"
},
"water_amount": { "water_amount": {
"name": "[%key:component::ecovacs::entity::number::water_amount::name%]", "name": "[%key:component::ecovacs::entity::number::water_amount::name%]",
"state": { "state": {

View File

@@ -7,7 +7,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyenphase"], "loggers": ["pyenphase"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyenphase==2.4.0"], "requirements": ["pyenphase==2.3.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_enphase-envoy._tcp.local." "type": "_enphase-envoy._tcp.local."

View File

@@ -396,7 +396,6 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None, int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None,
] ]
on_phase: str | None on_phase: str | None
cttype: str | None = None
CT_NET_CONSUMPTION_SENSORS = ( CT_NET_CONSUMPTION_SENSORS = (
@@ -410,7 +409,6 @@ CT_NET_CONSUMPTION_SENSORS = (
suggested_display_precision=3, suggested_display_precision=3,
value_fn=attrgetter("energy_delivered"), value_fn=attrgetter("energy_delivered"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="lifetime_net_production", key="lifetime_net_production",
@@ -422,7 +420,6 @@ CT_NET_CONSUMPTION_SENSORS = (
suggested_display_precision=3, suggested_display_precision=3,
value_fn=attrgetter("energy_received"), value_fn=attrgetter("energy_received"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="net_consumption", key="net_consumption",
@@ -434,7 +431,6 @@ CT_NET_CONSUMPTION_SENSORS = (
suggested_display_precision=3, suggested_display_precision=3,
value_fn=attrgetter("active_power"), value_fn=attrgetter("active_power"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="frequency", key="frequency",
@@ -446,7 +442,6 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"), value_fn=attrgetter("frequency"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="voltage", key="voltage",
@@ -459,7 +454,6 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"), value_fn=attrgetter("voltage"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="net_ct_current", key="net_ct_current",
@@ -472,7 +466,6 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("current"), value_fn=attrgetter("current"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="net_ct_powerfactor", key="net_ct_powerfactor",
@@ -483,7 +476,6 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"), value_fn=attrgetter("power_factor"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="net_consumption_ct_metering_status", key="net_consumption_ct_metering_status",
@@ -494,7 +486,6 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"), value_fn=attrgetter("metering_status"),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="net_consumption_ct_status_flags", key="net_consumption_ct_status_flags",
@@ -504,7 +495,6 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None, on_phase=None,
cttype=CtType.NET_CONSUMPTION,
), ),
) )
@@ -535,7 +525,6 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"), value_fn=attrgetter("frequency"),
on_phase=None, on_phase=None,
cttype=CtType.PRODUCTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="production_ct_voltage", key="production_ct_voltage",
@@ -548,7 +537,6 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"), value_fn=attrgetter("voltage"),
on_phase=None, on_phase=None,
cttype=CtType.PRODUCTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="production_ct_current", key="production_ct_current",
@@ -561,7 +549,6 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("current"), value_fn=attrgetter("current"),
on_phase=None, on_phase=None,
cttype=CtType.PRODUCTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="production_ct_powerfactor", key="production_ct_powerfactor",
@@ -572,7 +559,6 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"), value_fn=attrgetter("power_factor"),
on_phase=None, on_phase=None,
cttype=CtType.PRODUCTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="production_ct_metering_status", key="production_ct_metering_status",
@@ -583,7 +569,6 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"), value_fn=attrgetter("metering_status"),
on_phase=None, on_phase=None,
cttype=CtType.PRODUCTION,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="production_ct_status_flags", key="production_ct_status_flags",
@@ -593,7 +578,6 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None, on_phase=None,
cttype=CtType.PRODUCTION,
), ),
) )
@@ -623,7 +607,6 @@ CT_STORAGE_SENSORS = (
suggested_display_precision=3, suggested_display_precision=3,
value_fn=attrgetter("energy_delivered"), value_fn=attrgetter("energy_delivered"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="lifetime_battery_charged", key="lifetime_battery_charged",
@@ -635,7 +618,6 @@ CT_STORAGE_SENSORS = (
suggested_display_precision=3, suggested_display_precision=3,
value_fn=attrgetter("energy_received"), value_fn=attrgetter("energy_received"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="battery_discharge", key="battery_discharge",
@@ -647,7 +629,6 @@ CT_STORAGE_SENSORS = (
suggested_display_precision=3, suggested_display_precision=3,
value_fn=attrgetter("active_power"), value_fn=attrgetter("active_power"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="storage_ct_frequency", key="storage_ct_frequency",
@@ -659,7 +640,6 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"), value_fn=attrgetter("frequency"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="storage_voltage", key="storage_voltage",
@@ -672,7 +652,6 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"), value_fn=attrgetter("voltage"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="storage_ct_current", key="storage_ct_current",
@@ -685,7 +664,6 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("current"), value_fn=attrgetter("current"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="storage_ct_powerfactor", key="storage_ct_powerfactor",
@@ -696,7 +674,6 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"), value_fn=attrgetter("power_factor"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="storage_ct_metering_status", key="storage_ct_metering_status",
@@ -707,7 +684,6 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"), value_fn=attrgetter("metering_status"),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
EnvoyCTSensorEntityDescription( EnvoyCTSensorEntityDescription(
key="storage_ct_status_flags", key="storage_ct_status_flags",
@@ -717,7 +693,6 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None, on_phase=None,
cttype=CtType.STORAGE,
), ),
) )
@@ -1040,31 +1015,50 @@ async def async_setup_entry(
for description in NET_CONSUMPTION_PHASE_SENSORS[use_phase] for description in NET_CONSUMPTION_PHASE_SENSORS[use_phase]
if phase is not None if phase is not None
) )
# Add Current Transformer entities # Add net consumption CT entities
if envoy_data.ctmeters: if ctmeter := envoy_data.ctmeter_consumption:
entities.extend( entities.extend(
EnvoyCTEntity(coordinator, description) EnvoyConsumptionCTEntity(coordinator, description)
for sensors in ( for description in CT_NET_CONSUMPTION_SENSORS
CT_NET_CONSUMPTION_SENSORS, if ctmeter.measurement_type == CtType.NET_CONSUMPTION
CT_PRODUCTION_SENSORS,
CT_STORAGE_SENSORS,
)
for description in sensors
if description.cttype in envoy_data.ctmeters
) )
# Add Current Transformer phase entities # For each net consumption ct phase reported add net consumption entities
if ctmeters_phases := envoy_data.ctmeters_phases: if phase_data := envoy_data.ctmeter_consumption_phases:
entities.extend( entities.extend(
EnvoyCTPhaseEntity(coordinator, description) EnvoyConsumptionCTPhaseEntity(coordinator, description)
for sensors in ( for use_phase, phase in phase_data.items()
CT_NET_CONSUMPTION_PHASE_SENSORS, for description in CT_NET_CONSUMPTION_PHASE_SENSORS[use_phase]
CT_PRODUCTION_PHASE_SENSORS, if phase.measurement_type == CtType.NET_CONSUMPTION
CT_STORAGE_PHASE_SENSORS, )
) # Add production CT entities
for phase, descriptions in sensors.items() if ctmeter := envoy_data.ctmeter_production:
for description in descriptions entities.extend(
if (cttype := description.cttype) in ctmeters_phases EnvoyProductionCTEntity(coordinator, description)
and phase in ctmeters_phases[cttype] for description in CT_PRODUCTION_SENSORS
if ctmeter.measurement_type == CtType.PRODUCTION
)
# For each production ct phase reported add production ct entities
if phase_data := envoy_data.ctmeter_production_phases:
entities.extend(
EnvoyProductionCTPhaseEntity(coordinator, description)
for use_phase, phase in phase_data.items()
for description in CT_PRODUCTION_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.PRODUCTION
)
# Add storage CT entities
if ctmeter := envoy_data.ctmeter_storage:
entities.extend(
EnvoyStorageCTEntity(coordinator, description)
for description in CT_STORAGE_SENSORS
if ctmeter.measurement_type == CtType.STORAGE
)
# For each storage ct phase reported add storage ct entities
if phase_data := envoy_data.ctmeter_storage_phases:
entities.extend(
EnvoyStorageCTPhaseEntity(coordinator, description)
for use_phase, phase in phase_data.items()
for description in CT_STORAGE_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.STORAGE
) )
if envoy_data.inverters: if envoy_data.inverters:
@@ -1251,8 +1245,8 @@ class EnvoyNetConsumptionPhaseEntity(EnvoySystemSensorEntity):
return self.entity_description.value_fn(system_net_consumption) return self.entity_description.value_fn(system_net_consumption)
class EnvoyCTEntity(EnvoySystemSensorEntity): class EnvoyConsumptionCTEntity(EnvoySystemSensorEntity):
"""Envoy CT entity.""" """Envoy net consumption CT entity."""
entity_description: EnvoyCTSensorEntityDescription entity_description: EnvoyCTSensorEntityDescription
@@ -1261,13 +1255,13 @@ class EnvoyCTEntity(EnvoySystemSensorEntity):
self, self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None: ) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT sensor.""" """Return the state of the CT sensor."""
if (cttype := self.entity_description.cttype) not in self.data.ctmeters: if (ctmeter := self.data.ctmeter_consumption) is None:
return None return None
return self.entity_description.value_fn(self.data.ctmeters[cttype]) return self.entity_description.value_fn(ctmeter)
class EnvoyCTPhaseEntity(EnvoySystemSensorEntity): class EnvoyConsumptionCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy CT phase entity.""" """Envoy net consumption CT phase entity."""
entity_description: EnvoyCTSensorEntityDescription entity_description: EnvoyCTSensorEntityDescription
@@ -1278,14 +1272,78 @@ class EnvoyCTPhaseEntity(EnvoySystemSensorEntity):
"""Return the state of the CT phase sensor.""" """Return the state of the CT phase sensor."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.entity_description.on_phase assert self.entity_description.on_phase
if (cttype := self.entity_description.cttype) not in self.data.ctmeters_phases: if (ctmeter := self.data.ctmeter_consumption_phases) is None:
return None
if (phase := self.entity_description.on_phase) not in self.data.ctmeters_phases[
cttype
]:
return None return None
return self.entity_description.value_fn( return self.entity_description.value_fn(
self.data.ctmeters_phases[cttype][phase] ctmeter[self.entity_description.on_phase]
)
class EnvoyProductionCTEntity(EnvoySystemSensorEntity):
"""Envoy net consumption CT entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT sensor."""
if (ctmeter := self.data.ctmeter_production) is None:
return None
return self.entity_description.value_fn(ctmeter)
class EnvoyProductionCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy net consumption CT phase entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT phase sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
if (ctmeter := self.data.ctmeter_production_phases) is None:
return None
return self.entity_description.value_fn(
ctmeter[self.entity_description.on_phase]
)
class EnvoyStorageCTEntity(EnvoySystemSensorEntity):
"""Envoy net storage CT entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT sensor."""
if (ctmeter := self.data.ctmeter_storage) is None:
return None
return self.entity_description.value_fn(ctmeter)
class EnvoyStorageCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy net storage CT phase entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT phase sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
if (ctmeter := self.data.ctmeter_storage_phases) is None:
return None
return self.entity_description.value_fn(
ctmeter[self.entity_description.on_phase]
) )

View File

@@ -22,23 +22,19 @@ import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_ESPHOME,
SOURCE_IGNORE, SOURCE_IGNORE,
SOURCE_REAUTH, SOURCE_REAUTH,
SOURCE_RECONFIGURE, SOURCE_RECONFIGURE,
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
FlowType,
OptionsFlow, OptionsFlow,
) )
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
@@ -79,7 +75,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize flow.""" """Initialize flow."""
self._host: str | None = None self._host: str | None = None
self._connected_address: str | None = None
self.__name: str | None = None self.__name: str | None = None
self._port: int | None = None self._port: int | None = None
self._password: str | None = None self._password: str | None = None
@@ -503,55 +498,18 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
await self.hass.config_entries.async_remove( await self.hass.config_entries.async_remove(
self._entry_with_name_conflict.entry_id self._entry_with_name_conflict.entry_id
) )
return await self._async_create_entry() return self._async_create_entry()
async def _async_create_entry(self) -> ConfigFlowResult: @callback
def _async_create_entry(self) -> ConfigFlowResult:
"""Create the config entry.""" """Create the config entry."""
assert self._name is not None assert self._name is not None
assert self._device_info is not None
# Check if Z-Wave capabilities are present and start discovery flow
next_flow_id: str | None = None
if self._device_info.zwave_proxy_feature_flags:
assert self._connected_address is not None
assert self._port is not None
# Start Z-Wave discovery flow and get the flow ID
zwave_result = await self.hass.config_entries.flow.async_init(
"zwave_js",
context={
"source": SOURCE_ESPHOME,
"discovery_key": discovery_flow.DiscoveryKey(
domain=DOMAIN,
key=self._device_info.mac_address,
version=1,
),
},
data=ESPHomeServiceInfo(
name=self._device_info.name,
zwave_home_id=self._device_info.zwave_home_id or None,
ip_address=self._connected_address,
port=self._port,
noise_psk=self._noise_psk,
),
)
if zwave_result["type"] in (
FlowResultType.ABORT,
FlowResultType.CREATE_ENTRY,
):
_LOGGER.debug(
"Unable to continue created Z-Wave JS config flow: %s", zwave_result
)
else:
next_flow_id = zwave_result["flow_id"]
return self.async_create_entry( return self.async_create_entry(
title=self._name, title=self._name,
data=self._async_make_config_data(), data=self._async_make_config_data(),
options={ options={
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
}, },
next_flow=(FlowType.CONFIG_FLOW, next_flow_id) if next_flow_id else None,
) )
@callback @callback
@@ -598,7 +556,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if entry.data.get(CONF_DEVICE_NAME) == self._device_name: if entry.data.get(CONF_DEVICE_NAME) == self._device_name:
self._entry_with_name_conflict = entry self._entry_with_name_conflict = entry
return await self.async_step_name_conflict() return await self.async_step_name_conflict()
return await self._async_create_entry() return self._async_create_entry()
async def _async_reauth_validated_connection(self) -> ConfigFlowResult: async def _async_reauth_validated_connection(self) -> ConfigFlowResult:
"""Handle reauth validated connection.""" """Handle reauth validated connection."""
@@ -745,7 +703,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
try: try:
await cli.connect() await cli.connect()
self._device_info = await cli.device_info() self._device_info = await cli.device_info()
self._connected_address = cli.connected_address
except InvalidAuthAPIError: except InvalidAuthAPIError:
return ERROR_INVALID_PASSWORD_AUTH return ERROR_INVALID_PASSWORD_AUTH
except RequiresEncryptionAPIError: except RequiresEncryptionAPIError:

View File

@@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"], "mqtt": ["esphome/discover/#"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"aioesphomeapi==41.12.0", "aioesphomeapi==41.11.0",
"esphome-dashboard-api==1.3.0", "esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0" "bleak-esphome==3.3.0"
], ],
"zeroconf": ["_esphomelib._tcp.local."] "zeroconf": ["_esphomelib._tcp.local."]
} }

View File

@@ -67,7 +67,7 @@ def suitable_nextchange_time(device: FritzhomeDevice) -> bool:
def suitable_temperature(device: FritzhomeDevice) -> bool: def suitable_temperature(device: FritzhomeDevice) -> bool:
"""Check suitablity for temperature sensor.""" """Check suitablity for temperature sensor."""
return bool(device.has_temperature_sensor) return device.has_temperature_sensor and not device.has_thermostat
def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | None: def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | None:

View File

@@ -54,7 +54,7 @@ async def async_setup_entry(
except aiohttp.ClientResponseError as err: except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500: if 400 <= err.status < 500:
raise ConfigEntryAuthFailed( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required" "OAuth session is not valid, reauth required"
) from err ) from err
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
@@ -76,6 +76,10 @@ async def async_unload_entry(
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
) -> bool: ) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
for service_name in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service_name)
conversation.async_unset_agent(hass, entry) conversation.async_unset_agent(hass, entry)
return True return True

View File

@@ -26,7 +26,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
@@ -68,13 +68,7 @@ async def async_send_text_commands(
) -> list[CommandResponse]: ) -> list[CommandResponse]:
"""Send text commands to Google Assistant Service.""" """Send text commands to Google Assistant Service."""
# There can only be 1 entry (config_flow has single_instance_allowed) # There can only be 1 entry (config_flow has single_instance_allowed)
entries = hass.config_entries.async_loaded_entries(DOMAIN) entry: GoogleAssistantSDKConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
)
entry: GoogleAssistantSDKConfigEntry = entries[0]
session = entry.runtime_data.session session = entry.runtime_data.session
try: try:

View File

@@ -1,4 +1,4 @@
"""Services for the Google Assistant SDK integration.""" """Support for Google Assistant SDK."""
from __future__ import annotations from __future__ import annotations

View File

@@ -59,20 +59,14 @@
}, },
"media_player": { "media_player": {
"name": "Media player entity", "name": "Media player entity",
"description": "Name(s) of media player entities to play the Google Assistant's audio response on. This does not target the device for the command itself." "description": "Name(s) of media player entities to play response on."
} }
} }
} }
}, },
"exceptions": { "exceptions": {
"entry_not_loaded": {
"message": "Entry not loaded"
},
"grpc_error": { "grpc_error": {
"message": "Failed to communicate with Google Assistant" "message": "Failed to communicate with Google Assistant"
},
"reauth_required": {
"message": "Credentials are invalid, re-authentication required"
} }
} }
} }

View File

@@ -22,7 +22,6 @@ from homeassistant.exceptions import (
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
_UPLOAD_AND_DOWNLOAD_TIMEOUT = 12 * 3600 _UPLOAD_AND_DOWNLOAD_TIMEOUT = 12 * 3600
_UPLOAD_MAX_RETRIES = 20
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -151,7 +150,6 @@ class DriveClient:
backup_metadata, backup_metadata,
open_stream, open_stream,
backup.size, backup.size,
max_retries=_UPLOAD_MAX_RETRIES,
timeout=ClientTimeout(total=_UPLOAD_AND_DOWNLOAD_TIMEOUT), timeout=ClientTimeout(total=_UPLOAD_AND_DOWNLOAD_TIMEOUT),
) )
_LOGGER.debug( _LOGGER.debug(

View File

@@ -456,7 +456,6 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
"""Initialize the agent.""" """Initialize the agent."""
self.entry = entry self.entry = entry
self.subentry = subentry self.subentry = subentry
self.default_model = default_model
self._attr_name = subentry.title self._attr_name = subentry.title
self._genai_client = entry.runtime_data self._genai_client = entry.runtime_data
self._attr_unique_id = subentry.subentry_id self._attr_unique_id = subentry.subentry_id
@@ -490,7 +489,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
tools = tools or [] tools = tools or []
tools.append(Tool(google_search=GoogleSearch())) tools.append(Tool(google_search=GoogleSearch()))
model_name = options.get(CONF_CHAT_MODEL, self.default_model) model_name = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model> # Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
supports_system_instruction = ( supports_system_instruction = (
"gemma" not in model_name "gemma" not in model_name
@@ -621,7 +620,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
def create_generate_content_config(self) -> GenerateContentConfig: def create_generate_content_config(self) -> GenerateContentConfig:
"""Create the GenerateContentConfig for the LLM.""" """Create the GenerateContentConfig for the LLM."""
options = self.subentry.data options = self.subentry.data
model = options.get(CONF_CHAT_MODEL, self.default_model) model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
thinking_config: ThinkingConfig | None = None thinking_config: ThinkingConfig | None = None
if model.startswith("models/gemini-2.5") and not model.endswith( if model.startswith("models/gemini-2.5") and not model.endswith(
("tts", "image", "image-preview") ("tts", "image", "image-preview")

View File

@@ -22,7 +22,6 @@ from google.protobuf import timestamp_pb2
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -92,16 +91,6 @@ def convert_time(time_str: str) -> timestamp_pb2.Timestamp | None:
return timestamp return timestamp
SENSOR_DESCRIPTIONS = [
SensorEntityDescription(
key="duration",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
)
]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@@ -116,20 +105,20 @@ async def async_setup_entry(
client_options = ClientOptions(api_key=api_key) client_options = ClientOptions(api_key=api_key)
client = RoutesAsyncClient(client_options=client_options) client = RoutesAsyncClient(client_options=client_options)
sensors = [ sensor = GoogleTravelTimeSensor(
GoogleTravelTimeSensor( config_entry, name, api_key, origin, destination, client
config_entry, name, api_key, origin, destination, client, sensor_description )
)
for sensor_description in SENSOR_DESCRIPTIONS
]
async_add_entities(sensors, False) async_add_entities([sensor], False)
class GoogleTravelTimeSensor(SensorEntity): class GoogleTravelTimeSensor(SensorEntity):
"""Representation of a Google travel time sensor.""" """Representation of a Google travel time sensor."""
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_device_class = SensorDeviceClass.DURATION
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__( def __init__(
self, self,
@@ -139,10 +128,8 @@ class GoogleTravelTimeSensor(SensorEntity):
origin: str, origin: str,
destination: str, destination: str,
client: RoutesAsyncClient, client: RoutesAsyncClient,
sensor_description: SensorEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self.entity_description = sensor_description
self._attr_name = name self._attr_name = name
self._attr_unique_id = config_entry.entry_id self._attr_unique_id = config_entry.entry_id
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(

View File

@@ -73,6 +73,7 @@ class HassioAddonSwitch(HassioAddonEntity, SwitchEntity):
try: try:
await supervisor_client.addons.start_addon(self._addon_slug) await supervisor_client.addons.start_addon(self._addon_slug)
except SupervisorError as err: except SupervisorError as err:
_LOGGER.error("Failed to start addon %s: %s", self._addon_slug, err)
raise HomeAssistantError(err) from err raise HomeAssistantError(err) from err
await self.coordinator.force_addon_info_data_refresh(self._addon_slug) await self.coordinator.force_addon_info_data_refresh(self._addon_slug)

View File

@@ -10,7 +10,7 @@
"loggers": ["pyhap"], "loggers": ["pyhap"],
"requirements": [ "requirements": [
"HAP-python==5.0.0", "HAP-python==5.0.0",
"fnv-hash-fast==1.6.0", "fnv-hash-fast==1.5.0",
"PyQRCode==1.2.1", "PyQRCode==1.2.1",
"base36==0.1.1" "base36==0.1.1"
], ],

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.20"], "requirements": ["aiohomekit==3.2.19"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@@ -122,24 +122,11 @@ async def async_setup_entry(
coordinators.main.new_zones_callbacks.append(_add_new_zones) coordinators.main.new_zones_callbacks.append(_add_new_zones)
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_RESUME, None, "resume")
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_RESUME, SERVICE_START_WATERING, SCHEMA_START_WATERING, "start_watering"
None,
"resume",
entity_device_classes=(BinarySensorDeviceClass.RUNNING,),
)
platform.async_register_entity_service(
SERVICE_START_WATERING,
SCHEMA_START_WATERING,
"start_watering",
entity_device_classes=(BinarySensorDeviceClass.RUNNING,),
)
platform.async_register_entity_service(
SERVICE_SUSPEND,
SCHEMA_SUSPEND,
"suspend",
entity_device_classes=(BinarySensorDeviceClass.RUNNING,),
) )
platform.async_register_entity_service(SERVICE_SUSPEND, SCHEMA_SUSPEND, "suspend")
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):

View File

@@ -8,16 +8,13 @@ from idasen_ha import Desk
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator] type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator]
UPDATE_DEBOUNCE_TIME = 0.2
class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
"""Class to manage updates for the Idasen Desk.""" """Class to manage updates for the Idasen Desk."""
@@ -36,22 +33,9 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
hass, _LOGGER, config_entry=config_entry, name=config_entry.title hass, _LOGGER, config_entry=config_entry, name=config_entry.title
) )
self.address = address self.address = address
self.desk = Desk(self._async_handle_update)
self._expected_connected = False self._expected_connected = False
self._height: int | None = None
@callback self.desk = Desk(self.async_set_updated_data)
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=async_update_data,
)
async def async_connect(self) -> bool: async def async_connect(self) -> bool:
"""Connect to desk.""" """Connect to desk."""
@@ -76,9 +60,3 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
"""Ensure that the desk is connected if that is the expected state.""" """Ensure that the desk is connected if that is the expected state."""
if self._expected_connected: if self._expected_connected:
await self.async_connect() await self.async_connect()
@callback
def _async_handle_update(self, height: int | None) -> None:
"""Handle an update from the desk."""
self._height = height
self._debouncer.async_schedule_call()

View File

@@ -147,9 +147,8 @@ class KrakenData:
def _get_websocket_name_asset_pairs(self) -> str: def _get_websocket_name_asset_pairs(self) -> str:
return ",".join( return ",".join(
pair self.tradable_asset_pairs[tracked_pair]
for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS] for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS]
if (pair := self.tradable_asset_pairs.get(tracked_pair)) is not None
) )
def set_update_interval(self, update_interval: int) -> None: def set_update_interval(self, update_interval: int) -> None:

View File

@@ -156,7 +156,7 @@ async def async_setup_entry(
for description in SENSOR_TYPES for description in SENSOR_TYPES
] ]
) )
async_add_entities(entities) async_add_entities(entities, True)
_async_add_kraken_sensors(config_entry.options[CONF_TRACKED_ASSET_PAIRS]) _async_add_kraken_sensors(config_entry.options[CONF_TRACKED_ASSET_PAIRS])

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import cast from typing import cast
from pylamarzocco.const import BackFlushStatus, MachineState, ModelName, WidgetType from pylamarzocco.const import BackFlushStatus, ModelName, WidgetType
from pylamarzocco.models import ( from pylamarzocco.models import (
BackFlush, BackFlush,
BaseWidgetOutput, BaseWidgetOutput,
@@ -97,14 +97,7 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
).brewing_start_time ).brewing_start_time
), ),
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
available_fn=( available_fn=(lambda coordinator: not coordinator.websocket_terminated),
lambda coordinator: not coordinator.websocket_terminated
and cast(
MachineStatus,
coordinator.device.dashboard.config[WidgetType.CM_MACHINE_STATUS],
).status
is MachineState.BREWING
),
), ),
LaMarzoccoSensorEntityDescription( LaMarzoccoSensorEntityDescription(
key="steam_boiler_ready_time", key="steam_boiler_ready_time",

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.28.3", "ld2410-ble==0.1.1"] "requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
} }

View File

@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble", "documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.28.3", "led-ble==1.1.7"] "requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
} }

View File

@@ -7,6 +7,6 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["letpot"], "loggers": ["letpot"],
"quality_scale": "silver", "quality_scale": "bronze",
"requirements": ["letpot==0.6.2"] "requirements": ["letpot==0.6.2"]
} }

View File

@@ -41,10 +41,7 @@ rules:
docs-installation-parameters: done docs-installation-parameters: done
entity-unavailable: done entity-unavailable: done
integration-owner: done integration-owner: done
log-when-unavailable: log-when-unavailable: todo
status: done
comment: |
Logging handled by library when (un)available once (push) or coordinator (pull).
parallel-updates: done parallel-updates: done
reauthentication-flow: done reauthentication-flow: done
test-coverage: done test-coverage: done

View File

@@ -196,11 +196,11 @@ class LocalTodoListEntity(TodoListEntity):
item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)} item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)}
if uid not in item_idx: if uid not in item_idx:
raise HomeAssistantError( raise HomeAssistantError(
f"Item '{uid}' not found in todo list {self.entity_id}" "Item '{uid}' not found in todo list {self.entity_id}"
) )
if previous_uid and previous_uid not in item_idx: if previous_uid and previous_uid not in item_idx:
raise HomeAssistantError( raise HomeAssistantError(
f"Item '{previous_uid}' not found in todo list {self.entity_id}" "Item '{previous_uid}' not found in todo list {self.entity_id}"
) )
dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0 dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
src_idx = item_idx[uid] src_idx = item_idx[uid]

View File

@@ -88,17 +88,6 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterBinarySensor, entity_class=MatterBinarySensor,
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,), required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
), ),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatOccupancySensor",
device_class=BinarySensorDeviceClass.OCCUPANCY,
# The first bit = if occupied
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.Occupancy,),
),
MatterDiscoverySchema( MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR, platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription( entity_description=MatterBinarySensorEntityDescription(

View File

@@ -146,13 +146,6 @@
"off": "mdi:lock-off" "off": "mdi:lock-off"
} }
}, },
"speaker_mute": {
"default": "mdi:volume-high",
"state": {
"on": "mdi:volume-mute",
"off": "mdi:volume-high"
}
},
"evse_charging_switch": { "evse_charging_switch": {
"default": "mdi:ev-station" "default": "mdi:ev-station"
}, },

View File

@@ -176,7 +176,6 @@ DISCOVERY_SCHEMAS = [
), ),
entity_class=MatterNumber, entity_class=MatterNumber,
required_attributes=(clusters.LevelControl.Attributes.OnLevel,), required_attributes=(clusters.LevelControl.Attributes.OnLevel,),
not_device_type=(device_types.Speaker,),
# allow None value to account for 'default' value # allow None value to account for 'default' value
allow_none_value=True, allow_none_value=True,
), ),

View File

@@ -152,7 +152,6 @@ PUMP_CONTROL_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None, clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
} }
HUMIDITY_SCALING_FACTOR = 100
TEMPERATURE_SCALING_FACTOR = 100 TEMPERATURE_SCALING_FACTOR = 100
@@ -309,7 +308,7 @@ DISCOVERY_SCHEMAS = [
key="TemperatureSensor", key="TemperatureSensor",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR, device_to_ha=lambda x: x / 100,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
@@ -345,7 +344,7 @@ DISCOVERY_SCHEMAS = [
key="HumiditySensor", key="HumiditySensor",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
device_to_ha=lambda x: x / HUMIDITY_SCALING_FACTOR, device_to_ha=lambda x: x / 100,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
@@ -1137,7 +1136,7 @@ DISCOVERY_SCHEMAS = [
key="ThermostatLocalTemperature", key="ThermostatLocalTemperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR, device_to_ha=lambda x: x / 100,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,

View File

@@ -514,9 +514,6 @@
"power": { "power": {
"name": "Power" "name": "Power"
}, },
"speaker_mute": {
"name": "Mute"
},
"child_lock": { "child_lock": {
"name": "Child lock" "name": "Child lock"
}, },

View File

@@ -203,6 +203,7 @@ DISCOVERY_SCHEMAS = [
device_types.Refrigerator, device_types.Refrigerator,
device_types.RoboticVacuumCleaner, device_types.RoboticVacuumCleaner,
device_types.RoomAirConditioner, device_types.RoomAirConditioner,
device_types.Speaker,
), ),
), ),
MatterDiscoverySchema( MatterDiscoverySchema(
@@ -241,24 +242,6 @@ DISCOVERY_SCHEMAS = [
device_types.Speaker, device_types.Speaker,
), ),
), ),
MatterDiscoverySchema(
platform=Platform.SWITCH,
entity_description=MatterNumericSwitchEntityDescription(
key="MatterMuteToggle",
translation_key="speaker_mute",
device_to_ha={
True: False, # True means volume is on, so HA should show mute as off
False: True, # False means volume is off (muted), so HA should show mute as on
}.get,
ha_to_device={
False: True, # HA showing mute as off means volume is on, so send True
True: False, # HA showing mute as on means volume is off (muted), so send False
}.get,
),
entity_class=MatterNumericSwitch,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
device_type=(device_types.Speaker,),
),
MatterDiscoverySchema( MatterDiscoverySchema(
platform=Platform.SWITCH, platform=Platform.SWITCH,
entity_description=MatterNumericSwitchEntityDescription( entity_description=MatterNumericSwitchEntityDescription(

View File

@@ -1,16 +1,7 @@
"""Model Context Protocol transport protocol for Streamable HTTP and SSE. """Model Context Protocol transport protocol for Server Sent Events (SSE).
This registers HTTP endpoints that support the Streamable HTTP protocol as This registers HTTP endpoints that supports SSE as a transport layer
well as the older SSE as a transport layer. for the Model Context Protocol. There are two HTTP endpoints:
The Streamable HTTP protocol uses a single HTTP endpoint:
- /api/mcp_server: The Streamable HTTP endpoint currently implements the
stateless protocol for simplicity. This receives client requests and
sends them to the MCP server, then waits for a response to send back to
the client.
The older SSE protocol has two HTTP endpoints:
- /mcp_server/sse: The SSE endpoint that is used to establish a session - /mcp_server/sse: The SSE endpoint that is used to establish a session
with the client and glue to the MCP server. This is used to push responses with the client and glue to the MCP server. This is used to push responses
@@ -23,9 +14,6 @@ The older SSE protocol has two HTTP endpoints:
See https://modelcontextprotocol.io/docs/concepts/transports See https://modelcontextprotocol.io/docs/concepts/transports
""" """
import asyncio
from dataclasses import dataclass
from http import HTTPStatus
import logging import logging
from aiohttp import web from aiohttp import web
@@ -33,14 +21,13 @@ from aiohttp.web_exceptions import HTTPBadRequest, HTTPNotFound
from aiohttp_sse import sse_response from aiohttp_sse import sse_response
import anyio import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp import JSONRPCRequest, types from mcp import types
from mcp.server import InitializationOptions, Server
from mcp.shared.message import SessionMessage from mcp.shared.message import SessionMessage
from homeassistant.components import conversation from homeassistant.components import conversation
from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.const import CONF_LLM_HASS_API from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.core import Context, HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import llm from homeassistant.helpers import llm
from .const import DOMAIN from .const import DOMAIN
@@ -50,14 +37,6 @@ from .types import MCPServerConfigEntry
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Streamable HTTP endpoint
STREAMABLE_API = "/api/mcp"
TIMEOUT = 60 # Seconds
# Content types
CONTENT_TYPE_JSON = "application/json"
# Legacy SSE endpoint
SSE_API = f"/{DOMAIN}/sse" SSE_API = f"/{DOMAIN}/sse"
MESSAGES_API = f"/{DOMAIN}/messages/{{session_id}}" MESSAGES_API = f"/{DOMAIN}/messages/{{session_id}}"
@@ -67,7 +46,6 @@ def async_register(hass: HomeAssistant) -> None:
"""Register the websocket API.""" """Register the websocket API."""
hass.http.register_view(ModelContextProtocolSSEView()) hass.http.register_view(ModelContextProtocolSSEView())
hass.http.register_view(ModelContextProtocolMessagesView()) hass.http.register_view(ModelContextProtocolMessagesView())
hass.http.register_view(ModelContextProtocolStreamableView())
def async_get_config_entry(hass: HomeAssistant) -> MCPServerConfigEntry: def async_get_config_entry(hass: HomeAssistant) -> MCPServerConfigEntry:
@@ -88,52 +66,6 @@ def async_get_config_entry(hass: HomeAssistant) -> MCPServerConfigEntry:
return config_entries[0] return config_entries[0]
@dataclass
class Streams:
"""Pairs of streams for MCP server communication."""
# The MCP server reads from the read stream. The HTTP handler receives
# incoming client messages and writes the to the read_stream_writer.
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
# The MCP server writes to the write stream. The HTTP handler reads from
# the write stream and sends messages to the client.
write_stream: MemoryObjectSendStream[SessionMessage]
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
def create_streams() -> Streams:
"""Create a new pair of streams for MCP server communication."""
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
return Streams(
read_stream=read_stream,
read_stream_writer=read_stream_writer,
write_stream=write_stream,
write_stream_reader=write_stream_reader,
)
async def create_mcp_server(
hass: HomeAssistant, context: Context, entry: MCPServerConfigEntry
) -> tuple[Server, InitializationOptions]:
"""Initialize the MCP server to ensure it's ready to handle requests."""
llm_context = llm.LLMContext(
platform=DOMAIN,
context=context,
language="*",
assistant=conversation.DOMAIN,
device_id=None,
)
llm_api_id = entry.data[CONF_LLM_HASS_API]
server = await create_server(hass, llm_api_id, llm_context)
options = await hass.async_add_executor_job(
server.create_initialization_options # Reads package for version info
)
return server, options
class ModelContextProtocolSSEView(HomeAssistantView): class ModelContextProtocolSSEView(HomeAssistantView):
"""Model Context Protocol SSE endpoint.""" """Model Context Protocol SSE endpoint."""
@@ -154,12 +86,30 @@ class ModelContextProtocolSSEView(HomeAssistantView):
entry = async_get_config_entry(hass) entry = async_get_config_entry(hass)
session_manager = entry.runtime_data session_manager = entry.runtime_data
server, options = await create_mcp_server(hass, self.context(request), entry) context = llm.LLMContext(
streams = create_streams() platform=DOMAIN,
context=self.context(request),
language="*",
assistant=conversation.DOMAIN,
device_id=None,
)
llm_api_id = entry.data[CONF_LLM_HASS_API]
server = await create_server(hass, llm_api_id, context)
options = await hass.async_add_executor_job(
server.create_initialization_options # Reads package for version info
)
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
write_stream: MemoryObjectSendStream[SessionMessage]
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
async with ( async with (
sse_response(request) as response, sse_response(request) as response,
session_manager.create(Session(streams.read_stream_writer)) as session_id, session_manager.create(Session(read_stream_writer)) as session_id,
): ):
session_uri = MESSAGES_API.format(session_id=session_id) session_uri = MESSAGES_API.format(session_id=session_id)
_LOGGER.debug("Sending SSE endpoint: %s", session_uri) _LOGGER.debug("Sending SSE endpoint: %s", session_uri)
@@ -167,7 +117,7 @@ class ModelContextProtocolSSEView(HomeAssistantView):
async def sse_reader() -> None: async def sse_reader() -> None:
"""Forward MCP server responses to the client.""" """Forward MCP server responses to the client."""
async for session_message in streams.write_stream_reader: async for session_message in write_stream_reader:
_LOGGER.debug("Sending SSE message: %s", session_message) _LOGGER.debug("Sending SSE message: %s", session_message)
await response.send( await response.send(
session_message.message.model_dump_json( session_message.message.model_dump_json(
@@ -178,7 +128,7 @@ class ModelContextProtocolSSEView(HomeAssistantView):
async with anyio.create_task_group() as tg: async with anyio.create_task_group() as tg:
tg.start_soon(sse_reader) tg.start_soon(sse_reader)
await server.run(streams.read_stream, streams.write_stream, options) await server.run(read_stream, write_stream, options)
return response return response
@@ -218,64 +168,3 @@ class ModelContextProtocolMessagesView(HomeAssistantView):
_LOGGER.debug("Received client message: %s", message) _LOGGER.debug("Received client message: %s", message)
await session.read_stream_writer.send(SessionMessage(message)) await session.read_stream_writer.send(SessionMessage(message))
return web.Response(status=200) return web.Response(status=200)
class ModelContextProtocolStreamableView(HomeAssistantView):
"""Model Context Protocol Streamable HTTP endpoint."""
name = f"{DOMAIN}:streamable"
url = STREAMABLE_API
async def get(self, request: web.Request) -> web.StreamResponse:
"""Handle unsupported methods."""
return web.Response(
status=HTTPStatus.METHOD_NOT_ALLOWED, text="Only POST method is supported"
)
async def post(self, request: web.Request) -> web.StreamResponse:
"""Process JSON-RPC messages for the Model Context Protocol."""
hass = request.app[KEY_HASS]
entry = async_get_config_entry(hass)
# The request must include a JSON-RPC message
if CONTENT_TYPE_JSON not in request.headers.get("accept", ""):
raise HTTPBadRequest(text=f"Client must accept {CONTENT_TYPE_JSON}")
if request.content_type != CONTENT_TYPE_JSON:
raise HTTPBadRequest(text=f"Content-Type must be {CONTENT_TYPE_JSON}")
try:
json_data = await request.json()
message = types.JSONRPCMessage.model_validate(json_data)
except ValueError as err:
_LOGGER.debug("Failed to parse message as JSON-RPC message: %s", err)
raise HTTPBadRequest(text="Request must be a JSON-RPC message") from err
_LOGGER.debug("Received client message: %s", message)
# For notifications and responses only, return 202 Accepted
if not isinstance(message.root, JSONRPCRequest):
_LOGGER.debug("Notification or response received, returning 202")
return web.Response(status=HTTPStatus.ACCEPTED)
# The MCP server runs as a background task for the duration of the
# request. We open a buffered stream pair to communicate with it. The
# request is sent to the MCP server and we wait for a single response
# then shut down the server.
server, options = await create_mcp_server(hass, self.context(request), entry)
streams = create_streams()
async def run_server() -> None:
await server.run(
streams.read_stream, streams.write_stream, options, stateless=True
)
async with asyncio.timeout(TIMEOUT), anyio.create_task_group() as tg:
tg.start_soon(run_server)
await streams.read_stream_writer.send(SessionMessage(message))
session_message = await anext(streams.write_stream_reader)
tg.cancel_scope.cancel()
_LOGGER.debug("Sending response: %s", session_message)
return web.json_response(
data=session_message.message.model_dump(by_alias=True, exclude_none=True),
)

View File

@@ -48,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
), ),
) )
try: try:
await client.define_household_support()
about = await client.get_about() about = await client.get_about()
version = create_version(about.version) version = create_version(about.version)
except MealieAuthenticationError as error: except MealieAuthenticationError as error:

View File

@@ -19,4 +19,4 @@ ATTR_NOTE_TEXT = "note_text"
ATTR_SEARCH_TERMS = "search_terms" ATTR_SEARCH_TERMS = "search_terms"
ATTR_RESULT_LIMIT = "result_limit" ATTR_RESULT_LIMIT = "result_limit"
MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v2.0.0") MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0")

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/mealie", "documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "platinum", "quality_scale": "silver",
"requirements": ["aiomealie==1.0.0"] "requirements": ["aiomealie==0.11.0"]
} }

View File

@@ -49,11 +49,11 @@ rules:
The integration will discover a Mealie addon posting a discovery message. The integration will discover a Mealie addon posting a discovery message.
docs-data-update: done docs-data-update: done
docs-examples: done docs-examples: done
docs-known-limitations: done docs-known-limitations: todo
docs-supported-devices: done docs-supported-devices: todo
docs-supported-functions: done docs-supported-functions: done
docs-troubleshooting: done docs-troubleshooting: todo
docs-use-cases: done docs-use-cases: todo
dynamic-devices: dynamic-devices:
status: done status: done
comment: | comment: |

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/melcloud", "documentation": "https://www.home-assistant.io/integrations/melcloud",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pymelcloud"], "loggers": ["pymelcloud"],
"requirements": ["python-melcloud==0.1.2"] "requirements": ["python-melcloud==0.1.0"]
} }

View File

@@ -37,8 +37,8 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)}, identifiers={(DOMAIN, device_id)},
serial_number=device_id, serial_number=device_id,
name=device.device_name or appliance_type or device.tech_type, name=appliance_type or device.tech_type,
translation_key=None if device.device_name else appliance_type, translation_key=appliance_type,
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=device.tech_type, model=device.tech_type,
hw_version=device.xkm_tech_type, hw_version=device.xkm_tech_type,

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill", "documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["mill", "mill_local"], "loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.14.0", "mill-local==0.3.0"] "requirements": ["millheater==0.13.1", "mill-local==0.3.0"]
} }

View File

@@ -11,9 +11,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Min/Max from a config entry.""" """Set up Min/Max from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True return True
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -71,7 +71,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
config_flow = CONFIG_FLOW config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW options_flow = OPTIONS_FLOW
options_flow_reloads = True
def async_config_entry_title(self, options: Mapping[str, Any]) -> str: def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title.""" """Return config entry title."""

View File

@@ -40,7 +40,6 @@ from homeassistant.util.async_ import create_eager_task
from . import debug_info, discovery from . import debug_info, discovery
from .client import ( from .client import (
MQTT, MQTT,
async_await_subscription,
async_publish, async_publish,
async_subscribe, async_subscribe,
async_subscribe_internal, async_subscribe_internal,
@@ -160,7 +159,6 @@ __all__ = [
"PublishPayloadType", "PublishPayloadType",
"ReceiveMessage", "ReceiveMessage",
"SetupPhases", "SetupPhases",
"async_await_subscription",
"async_check_config_schema", "async_check_config_schema",
"async_create_certificate_temp_files", "async_create_certificate_temp_files",
"async_forward_entry_setup_and_setup_discovery", "async_forward_entry_setup_and_setup_discovery",

View File

@@ -38,10 +38,7 @@ from homeassistant.core import (
get_hassjob_callable_job_type, get_hassjob_callable_job_type,
) )
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import async_dispatcher_send
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.importlib import async_import_module
from homeassistant.helpers.start import async_at_started from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -74,7 +71,6 @@ from .const import (
DEFAULT_WS_PATH, DEFAULT_WS_PATH,
DOMAIN, DOMAIN,
MQTT_CONNECTION_STATE, MQTT_CONNECTION_STATE,
MQTT_PROCESSED_SUBSCRIPTIONS,
PROTOCOL_5, PROTOCOL_5,
PROTOCOL_31, PROTOCOL_31,
TRANSPORT_WEBSOCKETS, TRANSPORT_WEBSOCKETS,
@@ -113,7 +109,6 @@ INITIAL_SUBSCRIBE_COOLDOWN = 0.5
SUBSCRIBE_COOLDOWN = 0.1 SUBSCRIBE_COOLDOWN = 0.1
UNSUBSCRIBE_COOLDOWN = 0.1 UNSUBSCRIBE_COOLDOWN = 0.1
TIMEOUT_ACK = 10 TIMEOUT_ACK = 10
SUBSCRIBE_TIMEOUT = 10
RECONNECT_INTERVAL_SECONDS = 10 RECONNECT_INTERVAL_SECONDS = 10
MAX_WILDCARD_SUBSCRIBES_PER_CALL = 1 MAX_WILDCARD_SUBSCRIBES_PER_CALL = 1
@@ -189,64 +184,6 @@ async def async_publish(
) )
async def async_await_subscription(
hass: HomeAssistant,
topic: str,
qos: int = DEFAULT_QOS,
) -> None:
"""Wait for an MQTT subscription to be completed."""
subscription_complete: asyncio.Future[None]
async def _sync_mqtt_subscribe(subscriptions: list[tuple[str, int]]) -> None:
if (topic, qos) not in subscriptions:
return
subscription_complete.set_result(None)
def _async_timeout_subscribe() -> None:
if not subscription_complete.done():
subscription_complete.set_exception(TimeoutError)
try:
mqtt_data = hass.data[DATA_MQTT]
except KeyError as exc:
raise HomeAssistantError(
f"Cannot wait for subscription to topic '{topic}' QoS {qos}, "
"make sure MQTT is set up correctly",
translation_key="mqtt_not_setup_cannot_wait_for_subscribe",
translation_domain=DOMAIN,
translation_placeholders={"topic": topic, "qos": str(qos)},
) from exc
client = mqtt_data.client
if not client.is_active_subscription(topic):
raise HomeAssistantError(
f"Cannot find subscription to topic '{topic}' and QoS {qos}, "
"make sure the subscription is successful",
translation_key="mqtt_not_setup_cannot_find_subscription",
translation_domain=DOMAIN,
translation_placeholders={"topic": topic, "qos": str(qos)},
)
if not client.is_pending_subscription(topic):
# Existing non pending subscription are assumed to be completed already
return
subscription_complete = hass.loop.create_future()
dispatcher = async_dispatcher_connect(
hass, MQTT_PROCESSED_SUBSCRIPTIONS, _sync_mqtt_subscribe
)
try:
hass.loop.call_later(SUBSCRIBE_TIMEOUT, _async_timeout_subscribe)
await subscription_complete
except TimeoutError as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="subscribe_timeout",
) from exc
finally:
dispatcher()
return
@bind_hass @bind_hass
async def async_subscribe( async def async_subscribe(
hass: HomeAssistant, hass: HomeAssistant,
@@ -254,47 +191,11 @@ async def async_subscribe(
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None], msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
qos: int = DEFAULT_QOS, qos: int = DEFAULT_QOS,
encoding: str | None = DEFAULT_ENCODING, encoding: str | None = DEFAULT_ENCODING,
wait: bool = False,
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Subscribe to an MQTT topic. """Subscribe to an MQTT topic.
Call the return value to unsubscribe. Call the return value to unsubscribe.
""" """
subscription_complete: asyncio.Future[None]
async def _sync_mqtt_subscribe(subscriptions: list[tuple[str, int]]) -> None:
if (topic, qos) not in subscriptions:
return
subscription_complete.set_result(None)
def _async_timeout_subscribe() -> None:
if not subscription_complete.done():
subscription_complete.set_exception(TimeoutError)
if (
wait
and DATA_MQTT in hass.data
and not hass.data[DATA_MQTT].client.is_active_subscription(topic)
):
subscription_complete = hass.loop.create_future()
dispatcher = async_dispatcher_connect(
hass, MQTT_PROCESSED_SUBSCRIPTIONS, _sync_mqtt_subscribe
)
subscribe_callback = async_subscribe_internal(
hass, topic, msg_callback, qos, encoding
)
try:
hass.loop.call_later(SUBSCRIBE_TIMEOUT, _async_timeout_subscribe)
await subscription_complete
except TimeoutError as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="subscribe_timeout",
) from exc
finally:
dispatcher()
return subscribe_callback
return async_subscribe_internal(hass, topic, msg_callback, qos, encoding) return async_subscribe_internal(hass, topic, msg_callback, qos, encoding)
@@ -739,16 +640,12 @@ class MQTT:
if fileno > -1: if fileno > -1:
self.loop.remove_writer(sock) self.loop.remove_writer(sock)
def is_active_subscription(self, topic: str) -> bool: def _is_active_subscription(self, topic: str) -> bool:
"""Check if a topic has an active subscription.""" """Check if a topic has an active subscription."""
return topic in self._simple_subscriptions or any( return topic in self._simple_subscriptions or any(
other.topic == topic for other in self._wildcard_subscriptions other.topic == topic for other in self._wildcard_subscriptions
) )
def is_pending_subscription(self, topic: str) -> bool:
"""Check if a topic has a pending subscription."""
return topic in self._pending_subscriptions
async def async_publish( async def async_publish(
self, topic: str, payload: PublishPayloadType, qos: int, retain: bool self, topic: str, payload: PublishPayloadType, qos: int, retain: bool
) -> None: ) -> None:
@@ -1002,7 +899,7 @@ class MQTT:
@callback @callback
def _async_unsubscribe(self, topic: str) -> None: def _async_unsubscribe(self, topic: str) -> None:
"""Unsubscribe from a topic.""" """Unsubscribe from a topic."""
if self.is_active_subscription(topic): if self._is_active_subscription(topic):
if self._max_qos[topic] == 0: if self._max_qos[topic] == 0:
return return
subs = self._matching_subscriptions(topic) subs = self._matching_subscriptions(topic)
@@ -1066,7 +963,6 @@ class MQTT:
self._last_subscribe = time.monotonic() self._last_subscribe = time.monotonic()
await self._async_wait_for_mid_or_raise(mid, result) await self._async_wait_for_mid_or_raise(mid, result)
async_dispatcher_send(self.hass, MQTT_PROCESSED_SUBSCRIPTIONS, chunk_list)
async def _async_perform_unsubscribes(self) -> None: async def _async_perform_unsubscribes(self) -> None:
"""Perform pending MQTT client unsubscribes.""" """Perform pending MQTT client unsubscribes."""

View File

@@ -46,14 +46,6 @@ from homeassistant.components.light import (
VALID_COLOR_MODES, VALID_COLOR_MODES,
valid_supported_color_modes, valid_supported_color_modes,
) )
from homeassistant.components.number import (
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
DEFAULT_STEP,
DEVICE_CLASS_UNITS as NUMBER_DEVICE_CLASS_UNITS,
NumberDeviceClass,
NumberMode,
)
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
DEVICE_CLASS_UNITS, DEVICE_CLASS_UNITS,
@@ -74,7 +66,6 @@ from homeassistant.config_entries import (
from homeassistant.const import ( from homeassistant.const import (
ATTR_CONFIGURATION_URL, ATTR_CONFIGURATION_URL,
ATTR_HW_VERSION, ATTR_HW_VERSION,
ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_MODEL,
ATTR_MODEL_ID, ATTR_MODEL_ID,
ATTR_NAME, ATTR_NAME,
@@ -88,7 +79,6 @@ from homeassistant.const import (
CONF_EFFECT, CONF_EFFECT,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_HOST, CONF_HOST,
CONF_MODE,
CONF_NAME, CONF_NAME,
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
CONF_PASSWORD, CONF_PASSWORD,
@@ -221,9 +211,7 @@ from .const import (
CONF_IMAGE_TOPIC, CONF_IMAGE_TOPIC,
CONF_KEEPALIVE, CONF_KEEPALIVE,
CONF_LAST_RESET_VALUE_TEMPLATE, CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_MAX,
CONF_MAX_KELVIN, CONF_MAX_KELVIN,
CONF_MIN,
CONF_MIN_KELVIN, CONF_MIN_KELVIN,
CONF_MODE_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TEMPLATE,
CONF_MODE_COMMAND_TOPIC, CONF_MODE_COMMAND_TOPIC,
@@ -305,7 +293,6 @@ from .const import (
CONF_STATE_UNLOCKED, CONF_STATE_UNLOCKED,
CONF_STATE_UNLOCKING, CONF_STATE_UNLOCKING,
CONF_STATE_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE,
CONF_STEP,
CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUGGESTED_DISPLAY_PRECISION,
CONF_SUPPORTED_COLOR_MODES, CONF_SUPPORTED_COLOR_MODES,
CONF_SUPPORTED_FEATURES, CONF_SUPPORTED_FEATURES,
@@ -457,7 +444,6 @@ SUBENTRY_PLATFORMS = [
Platform.LIGHT, Platform.LIGHT,
Platform.LOCK, Platform.LOCK,
Platform.NOTIFY, Platform.NOTIFY,
Platform.NUMBER,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
] ]
@@ -693,24 +679,6 @@ LIGHT_SCHEMA_SELECTOR = SelectSelector(
translation_key="light_schema", translation_key="light_schema",
) )
) )
MIN_MAX_SELECTOR = NumberSelector(NumberSelectorConfig(step=1e-3))
NUMBER_DEVICE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[device_class.value for device_class in NumberDeviceClass],
mode=SelectSelectorMode.DROPDOWN,
# The number device classes are all shared with the sensor device classes
translation_key="device_class_sensor",
sort=True,
)
)
NUMBER_MODE_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[mode.value for mode in NumberMode],
mode=SelectSelectorMode.DROPDOWN,
translation_key="number_mode",
sort=True,
)
)
ON_COMMAND_TYPE_SELECTOR = SelectSelector( ON_COMMAND_TYPE_SELECTOR = SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=VALUES_ON_COMMAND_TYPE, options=VALUES_ON_COMMAND_TYPE,
@@ -758,7 +726,6 @@ SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
translation_key=CONF_STATE_CLASS, translation_key=CONF_STATE_CLASS,
) )
) )
STEP_SELECTOR = NumberSelector(NumberSelectorConfig(min=1e-3, step=1e-3))
SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector( SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=[platform.value for platform in VALID_COLOR_MODES], options=[platform.value for platform in VALID_COLOR_MODES],
@@ -915,23 +882,6 @@ def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
) )
@callback
def number_unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
"""Return a context based unit of measurement selector for number entities."""
if (
device_class := user_data.get(CONF_DEVICE_CLASS)
) is None or device_class not in NUMBER_DEVICE_CLASS_UNITS:
return TEXT_SELECTOR
return SelectSelector(
SelectSelectorConfig(
options=[str(uom) for uom in NUMBER_DEVICE_CLASS_UNITS[device_class]],
sort=True,
custom_value=True,
)
)
@callback @callback
def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]: def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
"""Run validator, then return the unmodified input.""" """Run validator, then return the unmodified input."""
@@ -1055,29 +1005,6 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
return errors return errors
@callback
def validate_number_platform_config(config: dict[str, Any]) -> dict[str, str]:
"""Validate MQTT number configuration."""
errors: dict[str, Any] = {}
if (
CONF_MIN in config
and CONF_MAX in config
and config[CONF_MIN] > config[CONF_MAX]
):
errors[CONF_MIN] = "max_below_min"
errors[CONF_MAX] = "max_below_min"
if (
(device_class := config.get(CONF_DEVICE_CLASS)) is not None
and device_class in NUMBER_DEVICE_CLASS_UNITS
and config.get(CONF_UNIT_OF_MEASUREMENT)
not in NUMBER_DEVICE_CLASS_UNITS[device_class]
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
return errors
@callback @callback
def validate_sensor_platform_config( def validate_sensor_platform_config(
config: dict[str, Any], config: dict[str, Any],
@@ -1140,7 +1067,6 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.LIGHT.value: validate_light_platform_config, Platform.LIGHT.value: validate_light_platform_config,
Platform.LOCK.value: None, Platform.LOCK.value: None,
Platform.NOTIFY.value: None, Platform.NOTIFY.value: None,
Platform.NUMBER.value: validate_number_platform_config,
Platform.SENSOR.value: validate_sensor_platform_config, Platform.SENSOR.value: validate_sensor_platform_config,
Platform.SWITCH.value: None, Platform.SWITCH.value: None,
} }
@@ -1356,17 +1282,6 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
}, },
Platform.LOCK.value: {}, Platform.LOCK.value: {},
Platform.NOTIFY.value: {}, Platform.NOTIFY.value: {},
Platform.NUMBER: {
CONF_DEVICE_CLASS: PlatformField(
selector=NUMBER_DEVICE_CLASS_SELECTOR,
required=False,
),
CONF_UNIT_OF_MEASUREMENT: PlatformField(
selector=number_unit_of_measurement_selector,
required=False,
custom_filtering=True,
),
},
Platform.SENSOR.value: { Platform.SENSOR.value: {
CONF_DEVICE_CLASS: PlatformField( CONF_DEVICE_CLASS: PlatformField(
selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False
@@ -3051,58 +2966,6 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
), ),
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
}, },
Platform.NUMBER.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_MIN: PlatformField(
selector=MIN_MAX_SELECTOR,
required=True,
default=DEFAULT_MIN_VALUE,
),
CONF_MAX: PlatformField(
selector=MIN_MAX_SELECTOR,
required=True,
default=DEFAULT_MAX_VALUE,
),
CONF_STEP: PlatformField(
selector=STEP_SELECTOR,
required=True,
default=DEFAULT_STEP,
),
CONF_MODE: PlatformField(
selector=NUMBER_MODE_SELECTOR,
required=True,
default=NumberMode.AUTO.value,
),
CONF_PAYLOAD_RESET: PlatformField(
selector=TEXT_SELECTOR,
required=False,
default=DEFAULT_PAYLOAD_RESET,
),
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
},
Platform.SENSOR.value: { Platform.SENSOR.value: {
CONF_STATE_TOPIC: PlatformField( CONF_STATE_TOPIC: PlatformField(
selector=TEXT_SELECTOR, selector=TEXT_SELECTOR,
@@ -3187,7 +3050,6 @@ MQTT_DEVICE_PLATFORM_FIELDS = {
), ),
ATTR_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False), ATTR_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False),
ATTR_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False), ATTR_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False),
ATTR_MANUFACTURER: PlatformField(selector=TEXT_SELECTOR, required=False),
ATTR_CONFIGURATION_URL: PlatformField( ATTR_CONFIGURATION_URL: PlatformField(
selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url" selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url"
), ),

View File

@@ -120,10 +120,8 @@ CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"
CONF_HUMIDITY_MAX = "max_humidity" CONF_HUMIDITY_MAX = "max_humidity"
CONF_HUMIDITY_MIN = "min_humidity" CONF_HUMIDITY_MIN = "min_humidity"
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template" CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
CONF_MAX = "max"
CONF_MAX_KELVIN = "max_kelvin" CONF_MAX_KELVIN = "max_kelvin"
CONF_MAX_MIREDS = "max_mireds" CONF_MAX_MIREDS = "max_mireds"
CONF_MIN = "min"
CONF_MIN_KELVIN = "min_kelvin" CONF_MIN_KELVIN = "min_kelvin"
CONF_MIN_MIREDS = "min_mireds" CONF_MIN_MIREDS = "min_mireds"
CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TEMPLATE = "mode_command_template"
@@ -198,7 +196,6 @@ CONF_STATE_OPENING = "state_opening"
CONF_STATE_STOPPED = "state_stopped" CONF_STATE_STOPPED = "state_stopped"
CONF_STATE_UNLOCKED = "state_unlocked" CONF_STATE_UNLOCKED = "state_unlocked"
CONF_STATE_UNLOCKING = "state_unlocking" CONF_STATE_UNLOCKING = "state_unlocking"
CONF_STEP = "step"
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template"
@@ -373,7 +370,6 @@ DOMAIN = "mqtt"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
MQTT_CONNECTION_STATE = "mqtt_connection_state" MQTT_CONNECTION_STATE = "mqtt_connection_state"
MQTT_PROCESSED_SUBSCRIPTIONS = "mqtt_processed_subscriptions"
PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None" PAYLOAD_NONE = "None"

View File

@@ -188,10 +188,7 @@ class MqttLock(MqttEntity, LockEntity):
return return
if payload == self._config[CONF_PAYLOAD_RESET]: if payload == self._config[CONF_PAYLOAD_RESET]:
# Reset the state to `unknown` # Reset the state to `unknown`
self._attr_is_locked = self._attr_is_locking = None self._attr_is_locked = None
self._attr_is_unlocking = None
self._attr_is_open = self._attr_is_opening = None
self._attr_is_jammed = None
elif payload in self._valid_states: elif payload in self._valid_states:
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED] self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING] self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]

View File

@@ -37,12 +37,8 @@ from .config import MQTT_RW_SCHEMA
from .const import ( from .const import (
CONF_COMMAND_TEMPLATE, CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_MAX,
CONF_MIN,
CONF_PAYLOAD_RESET, CONF_PAYLOAD_RESET,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_STEP,
DEFAULT_PAYLOAD_RESET,
) )
from .entity import MqttEntity, async_setup_entity_entry_helper from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import ( from .models import (
@@ -57,7 +53,12 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
CONF_MIN = "min"
CONF_MAX = "max"
CONF_STEP = "step"
DEFAULT_NAME = "MQTT Number" DEFAULT_NAME = "MQTT Number"
DEFAULT_PAYLOAD_RESET = "None"
MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
{ {

View File

@@ -165,15 +165,13 @@
"name": "[%key:common::config_flow::data::name%]", "name": "[%key:common::config_flow::data::name%]",
"configuration_url": "Configuration URL", "configuration_url": "Configuration URL",
"model": "Model", "model": "Model",
"model_id": "Model ID", "model_id": "Model ID"
"manufacturer": "Manufacturer"
}, },
"data_description": { "data_description": {
"name": "The name of the manually added MQTT device.", "name": "The name of the manually added MQTT device.",
"configuration_url": "A link to the webpage that can manage the configuration of this device. Can be either a 'http://', 'https://' or an internal 'homeassistant://' URL.", "configuration_url": "A link to the webpage that can manage the configuration of this device. Can be either a 'http://', 'https://' or an internal 'homeassistant://' URL.",
"model": "E.g. 'Cleanmaster Pro'.", "model": "E.g. 'Cleanmaster Pro'.",
"model_id": "E.g. '123NK2PRO'.", "model_id": "E.g. '123NK2PRO'."
"manufacturer": "E.g. Cleanmaster Ltd."
}, },
"sections": { "sections": {
"advanced_settings": { "advanced_settings": {
@@ -300,7 +298,7 @@
"suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)", "suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)",
"supported_features": "The features that the entity supports.", "supported_features": "The features that the entity supports.",
"temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.", "temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.",
"unit_of_measurement": "Defines the unit of measurement, if any." "unit_of_measurement": "Defines the unit of measurement of the sensor, if any."
}, },
"sections": { "sections": {
"advanced_settings": { "advanced_settings": {
@@ -336,9 +334,6 @@
"image_encoding": "Image encoding", "image_encoding": "Image encoding",
"image_topic": "Image topic", "image_topic": "Image topic",
"last_reset_value_template": "Last reset value template", "last_reset_value_template": "Last reset value template",
"max": "Maximum",
"min": "Minimum",
"mode": "Mode",
"modes": "Supported operation modes", "modes": "Supported operation modes",
"mode_command_topic": "Operation mode command topic", "mode_command_topic": "Operation mode command topic",
"mode_command_template": "Operation mode command template", "mode_command_template": "Operation mode command template",
@@ -349,7 +344,6 @@
"payload_off": "Payload \"off\"", "payload_off": "Payload \"off\"",
"payload_on": "Payload \"on\"", "payload_on": "Payload \"on\"",
"payload_press": "Payload \"press\"", "payload_press": "Payload \"press\"",
"payload_reset": "Payload \"reset\"",
"qos": "QoS", "qos": "QoS",
"red_template": "Red template", "red_template": "Red template",
"retain": "Retain", "retain": "Retain",
@@ -358,7 +352,6 @@
"state_template": "State template", "state_template": "State template",
"state_topic": "State topic", "state_topic": "State topic",
"state_value_template": "State value template", "state_value_template": "State value template",
"step": "Step",
"supported_color_modes": "Supported color modes", "supported_color_modes": "Supported color modes",
"url_template": "URL template", "url_template": "URL template",
"url_topic": "URL topic", "url_topic": "URL topic",
@@ -383,9 +376,6 @@
"image_encoding": "Select the encoding of the received image data", "image_encoding": "Select the encoding of the received image data",
"image_topic": "The MQTT topic subscribed to receive messages containing the image data. [Learn more.]({url}#image_topic)", "image_topic": "The MQTT topic subscribed to receive messages containing the image data. [Learn more.]({url}#image_topic)",
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
"max": "Maximum value. [Learn more.]({url}#max)",
"min": "Minimum value. [Learn more.]({url}#min)",
"mode": "Control how the number should be displayed in the UI. [Learn more.]({url}#mode)",
"modes": "A list of supported operation modes. [Learn more.]({url}#modes)", "modes": "A list of supported operation modes. [Learn more.]({url}#modes)",
"mode_command_topic": "The MQTT topic to publish commands to change the climate operation mode. [Learn more.]({url}#mode_command_topic)", "mode_command_topic": "The MQTT topic to publish commands to change the climate operation mode. [Learn more.]({url}#mode_command_topic)",
"mode_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the operation mode to be sent to the operation mode command topic. [Learn more.]({url}#mode_command_template)", "mode_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the operation mode to be sent to the operation mode command topic. [Learn more.]({url}#mode_command_template)",
@@ -396,7 +386,6 @@
"payload_off": "The payload that represents the \"off\" state.", "payload_off": "The payload that represents the \"off\" state.",
"payload_on": "The payload that represents the \"on\" state.", "payload_on": "The payload that represents the \"on\" state.",
"payload_press": "The payload to send when the button is triggered.", "payload_press": "The payload to send when the button is triggered.",
"payload_reset": "The payload received at the state topic that resets the entity to an unknown state.",
"qos": "The QoS value a {platform} entity should use.", "qos": "The QoS value a {platform} entity should use.",
"red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.", "red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.",
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
@@ -404,7 +393,6 @@
"state_on": "The incoming payload that represents the \"on\" state. Use only when the value that represents \"on\" state in the state topic is different from value that should be sent to the command topic to turn the device on.", "state_on": "The incoming payload that represents the \"on\" state. Use only when the value that represents \"on\" state in the state topic is different from value that should be sent to the command topic to turn the device on.",
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.", "state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)", "state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
"step": "Step value. Smallest value 0.001.",
"supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", "supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
"url_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract an URL from the received URL topic payload value. [Learn more.]({url}#url_template)", "url_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract an URL from the received URL topic payload value. [Learn more.]({url}#url_template)",
"url_topic": "The MQTT topic subscribed to receive messages containing the image URL. [Learn more.]({url}#url_topic)", "url_topic": "The MQTT topic subscribed to receive messages containing the image URL. [Learn more.]({url}#url_topic)",
@@ -1007,7 +995,6 @@
"invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list", "invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_url": "Invalid URL", "invalid_url": "Invalid URL",
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only", "last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
"max_below_min": "Max value should be greater or equal to min value",
"max_below_min_humidity": "Max humidity value should be greater than min humidity value", "max_below_min_humidity": "Max humidity value should be greater than min humidity value",
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value", "max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",
"max_below_min_temperature": "Max temperature value should be greater than min temperature value", "max_below_min_temperature": "Max temperature value should be greater than min temperature value",
@@ -1307,13 +1294,6 @@
"template": "Template" "template": "Template"
} }
}, },
"number_mode": {
"options": {
"auto": "[%key:component::number::entity_component::_::state_attributes::mode::state::auto%]",
"box": "[%key:component::number::entity_component::_::state_attributes::mode::state::box%]",
"slider": "[%key:component::number::entity_component::_::state_attributes::mode::state::slider%]"
}
},
"on_command_type": { "on_command_type": {
"options": { "options": {
"brightness": "Brightness", "brightness": "Brightness",
@@ -1333,7 +1313,6 @@
"light": "[%key:component::light::title%]", "light": "[%key:component::light::title%]",
"lock": "[%key:component::lock::title%]", "lock": "[%key:component::lock::title%]",
"notify": "[%key:component::notify::title%]", "notify": "[%key:component::notify::title%]",
"number": "[%key:component::number::title%]",
"sensor": "[%key:component::sensor::title%]", "sensor": "[%key:component::sensor::title%]",
"switch": "[%key:component::switch::title%]" "switch": "[%key:component::switch::title%]"
} }
@@ -1456,12 +1435,6 @@
"mqtt_not_setup_cannot_subscribe": { "mqtt_not_setup_cannot_subscribe": {
"message": "Cannot subscribe to topic \"{topic}\", make sure MQTT is set up correctly." "message": "Cannot subscribe to topic \"{topic}\", make sure MQTT is set up correctly."
}, },
"mqtt_not_setup_cannot_wait_for_subscribe": {
"message": "Cannot wait for subscription to topic \"{topic}\" and QoS {qos}, make sure MQTT is set up correctly."
},
"mqtt_not_setup_cannot_find_subscription": {
"message": "Cannot find subscription to topic \"{topic}\" and QoS {qos}, make sure the subscription is successful."
},
"mqtt_not_setup_cannot_publish": { "mqtt_not_setup_cannot_publish": {
"message": "Cannot publish to topic \"{topic}\", make sure MQTT is set up correctly." "message": "Cannot publish to topic \"{topic}\", make sure MQTT is set up correctly."
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.0.1"]
}

View File

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

View File

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

View File

@@ -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."
}
}
}

View File

@@ -8,6 +8,6 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pynordpool"], "loggers": ["pynordpool"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pynordpool==0.3.1"], "requirements": ["pynordpool==0.3.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

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

View File

@@ -163,7 +163,7 @@ SENSOR_DESCRIPTIONS: tuple[NtfySensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
native_unit_of_measurement=UnitOfInformation.BYTES, native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES,
suggested_display_precision=2, suggested_display_precision=0,
), ),
NtfySensorEntityDescription( NtfySensorEntityDescription(
key=NtfySensor.ATTACHMENT_TOTAL_SIZE_REMAINING, key=NtfySensor.ATTACHMENT_TOTAL_SIZE_REMAINING,
@@ -172,7 +172,7 @@ SENSOR_DESCRIPTIONS: tuple[NtfySensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
native_unit_of_measurement=UnitOfInformation.BYTES, native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES,
suggested_display_precision=2, suggested_display_precision=0,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
NtfySensorEntityDescription( NtfySensorEntityDescription(

View File

@@ -22,7 +22,7 @@
"name": "Mode", "name": "Mode",
"state": { "state": {
"auto": "Automatic", "auto": "Automatic",
"box": "Input field", "box": "Box",
"slider": "Slider" "slider": "Slider"
} }
}, },

View File

@@ -95,7 +95,6 @@ def _convert_content(
return ollama.Message( return ollama.Message(
role=MessageRole.ASSISTANT.value, role=MessageRole.ASSISTANT.value,
content=chat_content.content, content=chat_content.content,
thinking=chat_content.thinking_content,
tool_calls=[ tool_calls=[
ollama.Message.ToolCall( ollama.Message.ToolCall(
function=ollama.Message.ToolCall.Function( function=ollama.Message.ToolCall.Function(
@@ -104,8 +103,7 @@ def _convert_content(
) )
) )
for tool_call in chat_content.tool_calls or () for tool_call in chat_content.tool_calls or ()
] ],
or None,
) )
if isinstance(chat_content, conversation.UserContent): if isinstance(chat_content, conversation.UserContent):
images: list[ollama.Image] = [] images: list[ollama.Image] = []
@@ -164,8 +162,6 @@ async def _transform_stream(
] ]
if (content := response_message.get("content")) is not None: if (content := response_message.get("content")) is not None:
chunk["content"] = content chunk["content"] = content
if (thinking := response_message.get("thinking")) is not None:
chunk["thinking_content"] = thinking
if response_message.get("done"): if response_message.get("done"):
new_msg = True new_msg = True
yield chunk yield chunk

View File

@@ -35,8 +35,7 @@ from .const import CONF_DELETE_PERMANENTLY, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .coordinator import OneDriveConfigEntry from .coordinator import OneDriveConfigEntry
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MAX_CHUNK_SIZE = 60 * 1024 * 1024 # largest chunk possible, must be <= 60 MiB UPLOAD_CHUNK_SIZE = 32 * 320 * 1024 # 10.4MB
TARGET_CHUNKS = 20
TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours
METADATA_VERSION = 2 METADATA_VERSION = 2
CACHE_TTL = 300 CACHE_TTL = 300
@@ -162,21 +161,11 @@ class OneDriveBackupAgent(BackupAgent):
self._folder_id, self._folder_id,
await open_stream(), await open_stream(),
) )
# determine chunk based on target chunks
upload_chunk_size = backup.size / TARGET_CHUNKS
# find the nearest multiple of 320KB
upload_chunk_size = round(upload_chunk_size / (320 * 1024)) * (320 * 1024)
# limit to max chunk size
upload_chunk_size = min(upload_chunk_size, MAX_CHUNK_SIZE)
# ensure minimum chunk size of 320KB
upload_chunk_size = max(upload_chunk_size, 320 * 1024)
try: try:
backup_file = await LargeFileUploadClient.upload( backup_file = await LargeFileUploadClient.upload(
self._token_function, self._token_function,
file, file,
upload_chunk_size=upload_chunk_size, upload_chunk_size=UPLOAD_CHUNK_SIZE,
session=async_get_clientsession(self._hass), session=async_get_clientsession(self._hass),
) )
except HashMismatchError as err: except HashMismatchError as err:

View File

@@ -17,7 +17,6 @@ from homeassistant.util.enum import try_parse_enum
from .const import DOMAIN from .const import DOMAIN
from .device import ONVIFDevice from .device import ONVIFDevice
from .entity import ONVIFBaseEntity from .entity import ONVIFBaseEntity
from .util import build_event_entity_names
async def async_setup_entry( async def async_setup_entry(
@@ -25,45 +24,36 @@ async def async_setup_entry(
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up ONVIF binary sensor platform.""" """Set up a ONVIF binary sensor."""
device: ONVIFDevice = hass.data[DOMAIN][config_entry.unique_id] device: ONVIFDevice = hass.data[DOMAIN][config_entry.unique_id]
events = device.events.get_platform("binary_sensor") entities = {
entity_names = build_event_entity_names(events) event.uid: ONVIFBinarySensor(event.uid, device)
for event in device.events.get_platform("binary_sensor")
uids = set() }
entities = []
for event in events:
uids.add(event.uid)
entities.append(
ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
)
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id): 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: if entry.domain == "binary_sensor" and entry.unique_id not in entities:
uids.add(entry.unique_id) entities[entry.unique_id] = ONVIFBinarySensor(
entities.append(ONVIFBinarySensor(entry.unique_id, device, entry=entry)) entry.unique_id, device, entry
)
async_add_entities(entities) async_add_entities(entities.values())
uids_by_platform = device.events.get_uids_by_platform("binary_sensor") uids_by_platform = device.events.get_uids_by_platform("binary_sensor")
@callback @callback
def async_check_entities() -> None: def async_check_entities() -> None:
"""Check if we have added an entity for the event.""" """Check if we have added an entity for the event."""
nonlocal uids_by_platform nonlocal uids_by_platform
if not (missing := uids_by_platform.difference(uids)): if not (missing := uids_by_platform.difference(entities)):
return return
new_entities: dict[str, ONVIFBinarySensor] = {
events = device.events.get_platform("binary_sensor") uid: ONVIFBinarySensor(uid, device) for uid in missing
entity_names = build_event_entity_names(events) }
new_entities = [
ONVIFBinarySensor(uid, device, name=entity_names[uid]) for uid in missing
]
if new_entities: if new_entities:
uids.update(missing) entities.update(new_entities)
async_add_entities(new_entities) async_add_entities(new_entities.values())
device.events.async_add_listener(async_check_entities) device.events.async_add_listener(async_check_entities)
@@ -75,11 +65,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity):
_attr_unique_id: str _attr_unique_id: str
def __init__( def __init__(
self, self, uid: str, device: ONVIFDevice, entry: er.RegistryEntry | None = None
uid: str,
device: ONVIFDevice,
name: str | None = None,
entry: er.RegistryEntry | None = None,
) -> None: ) -> None:
"""Initialize the ONVIF binary sensor.""" """Initialize the ONVIF binary sensor."""
self._attr_unique_id = uid self._attr_unique_id = uid
@@ -92,13 +78,12 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity):
else: else:
event = device.events.get_uid(uid) event = device.events.get_uid(uid)
assert event assert event
assert name
self._attr_device_class = try_parse_enum( self._attr_device_class = try_parse_enum(
BinarySensorDeviceClass, event.device_class BinarySensorDeviceClass, event.device_class
) )
self._attr_entity_category = event.entity_category self._attr_entity_category = event.entity_category
self._attr_entity_registry_enabled_default = event.entity_enabled self._attr_entity_registry_enabled_default = event.entity_enabled
self._attr_name = f"{device.name} {name}" self._attr_name = f"{device.name} {event.name}"
self._attr_is_on = event.value self._attr_is_on = event.value
super().__init__(device) super().__init__(device)

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