Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
3913039490 Add sidebar default visible flag to panels 2025-10-30 16:51:56 +01:00
144 changed files with 582 additions and 4674 deletions

View File

@@ -361,7 +361,6 @@ homeassistant.components.myuplink.*
homeassistant.components.nam.*
homeassistant.components.nanoleaf.*
homeassistant.components.nasweb.*
homeassistant.components.neato.*
homeassistant.components.nest.*
homeassistant.components.netatmo.*
homeassistant.components.network.*

2
Dockerfile generated
View File

@@ -31,7 +31,7 @@ RUN \
&& go2rtc --version
# Install uv
RUN pip3 install uv==0.9.6
RUN pip3 install uv==0.9.5
WORKDIR /usr/src

View File

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

View File

@@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="daily_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
@@ -150,7 +150,7 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_LIGHTNING_PER_DAY,
translation_key="lightning_strikes_per_day",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
@@ -182,7 +182,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="monthly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
@@ -229,7 +229,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="weekly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
@@ -262,7 +262,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="yearly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.4.0"]
"requirements": ["hassil==3.3.0"]
}

View File

@@ -189,7 +189,7 @@ class BryantEvolutionClimate(ClimateEntity):
return HVACAction.HEATING
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="failed_to_parse_hvac_action",
translation_key="failed_to_parse_hvac_mode",
translation_placeholders={
"mode_and_active": mode_and_active,
"current_temperature": str(self.current_temperature),

View File

@@ -24,7 +24,7 @@
},
"exceptions": {
"failed_to_parse_hvac_action": {
"message": "Could not determine HVAC action: {mode_and_active}, {current_temperature}, {target_temperature_low}"
"message": "Could not determine HVAC action: {mode_and_active}, {self.current_temperature}, {self.target_temperature_low}"
},
"failed_to_parse_hvac_mode": {
"message": "Cannot parse response to HVACMode: {mode}"

View File

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

View File

@@ -768,16 +768,7 @@ class DefaultAgent(ConversationEntity):
if lang_intents.fuzzy_matcher is None:
return None
context_area: str | None = None
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
if satellite_area:
context_area = satellite_area.name
fuzzy_result = lang_intents.fuzzy_matcher.match(
user_input.text, context_area=context_area
)
fuzzy_result = lang_intents.fuzzy_matcher.match(user_input.text)
if fuzzy_result is None:
return None
@@ -1249,14 +1240,15 @@ class DefaultAgent(ConversationEntity):
intent_slot_list_names=self._fuzzy_config.slot_list_names,
slot_combinations={
intent_name: {
combo_key: SlotCombinationInfo(
context_area=combo_info.context_area,
name_domains=(
set(combo_info.name_domains)
if combo_info.name_domains
else None
),
)
combo_key: [
SlotCombinationInfo(
name_domains=(
set(combo_info.name_domains)
if combo_info.name_domains
else None
)
)
]
for combo_key, combo_info in intent_combos.items()
}
for intent_name, intent_combos in self._fuzzy_config.slot_combinations.items()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.4.0", "home-assistant-intents==2025.10.28"]
"requirements": ["hassil==3.3.0", "home-assistant-intents==2025.10.28"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pycync==0.4.3"]
"requirements": ["pycync==0.4.2"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==16.3.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==16.1.0"]
}

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["eheimdigital"],
"quality_scale": "platinum",
"requirements": ["eheimdigital==1.4.0"],
"requirements": ["eheimdigital==1.3.0"],
"zeroconf": [
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
]

View File

@@ -0,0 +1 @@
"""Virtual integration: Enmax Energy."""

View File

@@ -0,0 +1,6 @@
{
"domain": "enmax",
"name": "Enmax Energy",
"integration_type": "virtual",
"supported_by": "opower"
}

View File

@@ -40,9 +40,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
client = Firefly(
api_url=data[CONF_URL],
api_key=data[CONF_API_KEY],
session=async_get_clientsession(
hass=hass, verify_ssl=data[CONF_VERIFY_SSL]
),
session=async_get_clientsession(hass),
)
await client.get_about()
except FireflyAuthenticationError:

View File

@@ -263,6 +263,9 @@ class Panel:
# Title to show in the sidebar
sidebar_title: str | None = None
# If the panel should be visible by default in the sidebar
sidebar_default_visible: bool = True
# Url to show the panel in the frontend
frontend_url_path: str
@@ -280,6 +283,7 @@ class Panel:
component_name: str,
sidebar_title: str | None,
sidebar_icon: str | None,
sidebar_default_visible: bool,
frontend_url_path: str | None,
config: dict[str, Any] | None,
require_admin: bool,
@@ -293,6 +297,7 @@ class Panel:
self.config = config
self.require_admin = require_admin
self.config_panel_domain = config_panel_domain
self.sidebar_default_visible = sidebar_default_visible
@callback
def to_response(self) -> PanelResponse:
@@ -301,6 +306,7 @@ class Panel:
"component_name": self.component_name,
"icon": self.sidebar_icon,
"title": self.sidebar_title,
"default_visible": self.sidebar_default_visible,
"config": self.config,
"url_path": self.frontend_url_path,
"require_admin": self.require_admin,
@@ -315,6 +321,7 @@ def async_register_built_in_panel(
component_name: str,
sidebar_title: str | None = None,
sidebar_icon: str | None = None,
sidebar_default_visible: bool = True,
frontend_url_path: str | None = None,
config: dict[str, Any] | None = None,
require_admin: bool = False,
@@ -327,6 +334,7 @@ def async_register_built_in_panel(
component_name,
sidebar_title,
sidebar_icon,
sidebar_default_visible,
frontend_url_path,
config,
require_admin,
@@ -453,7 +461,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.http.app.router.register_resource(IndexView(repo_path, hass))
async_register_built_in_panel(hass, "light")
async_register_built_in_panel(hass, "security")
async_register_built_in_panel(hass, "safety")
async_register_built_in_panel(hass, "climate")
async_register_built_in_panel(hass, "profile")
@@ -879,6 +887,7 @@ class PanelResponse(TypedDict):
component_name: str
icon: str | None
title: str | None
default_visible: bool
config: dict[str, Any] | None
url_path: str
require_admin: bool

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251104.0"]
"requirements": ["home-assistant-frontend==20251001.4"]
}

View File

@@ -620,11 +620,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
# Unload coordinator
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
coordinator.unload()
# Pop coordinator
# Pop add-on data
hass.data.pop(ADDONS_COORDINATOR, None)
return unload_ok

View File

@@ -563,8 +563,3 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
self.async_set_updated_data(data)
except SupervisorError as err:
_LOGGER.warning("Could not refresh info for %s: %s", addon_slug, err)
@callback
def unload(self) -> None:
"""Clean up when config entry unloaded."""
self.jobs.unload()

View File

@@ -44,6 +44,7 @@ from .const import (
EVENT_SUPPORTED_CHANGED,
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DEPRECATED,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
@@ -86,6 +87,7 @@ ISSUE_KEYS_FOR_REPAIRS = {
"issue_system_disk_lifetime",
ISSUE_KEY_SYSTEM_FREE_SPACE,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_ADDON_DEPRECATED,
}
_LOGGER = logging.getLogger(__name__)

View File

@@ -3,7 +3,6 @@
from collections.abc import Callable
from dataclasses import dataclass, replace
from functools import partial
import logging
from typing import Any
from uuid import UUID
@@ -30,8 +29,6 @@ from .const import (
)
from .handler import get_supervisor_client
_LOGGER = logging.getLogger(__name__)
@dataclass(slots=True, frozen=True)
class JobSubscription:
@@ -48,7 +45,7 @@ class JobSubscription:
event_callback: Callable[[Job], Any]
uuid: str | None = None
name: str | None = None
reference: str | None = None
reference: str | None | type[Any] = Any
def __post_init__(self) -> None:
"""Validate at least one filter option is present."""
@@ -61,7 +58,7 @@ class JobSubscription:
"""Return true if job matches subscription filters."""
if self.uuid:
return job.uuid == self.uuid
return job.name == self.name and self.reference in (None, job.reference)
return job.name == self.name and self.reference in (Any, job.reference)
class SupervisorJobs:
@@ -73,7 +70,6 @@ class SupervisorJobs:
self._supervisor_client = get_supervisor_client(hass)
self._jobs: dict[UUID, Job] = {}
self._subscriptions: set[JobSubscription] = set()
self._dispatcher_disconnect: Callable[[], None] | None = None
@property
def current_jobs(self) -> list[Job]:
@@ -83,24 +79,20 @@ class SupervisorJobs:
def subscribe(self, subscription: JobSubscription) -> CALLBACK_TYPE:
"""Subscribe to updates for job. Return callback is used to unsubscribe.
If any jobs match the subscription at the time this is called, runs the
callback on them.
If any jobs match the subscription at the time this is called, creates
tasks to run their callback on it.
"""
self._subscriptions.add(subscription)
# Run the callback on each existing match
# We catch all errors to prevent an error in one from stopping the others
for match in [job for job in self._jobs.values() if subscription.matches(job)]:
try:
return subscription.event_callback(match)
except Exception as err: # noqa: BLE001
_LOGGER.error(
"Error encountered processing Supervisor Job (%s %s %s) - %s",
match.name,
match.reference,
match.uuid,
err,
)
# As these are callbacks they are safe to run in the event loop
# We wrap these in an asyncio task so subscribing does not wait on the logic
if matches := [job for job in self._jobs.values() if subscription.matches(job)]:
async def event_callback_async(job: Job) -> Any:
return subscription.event_callback(job)
for match in matches:
self._hass.async_create_task(event_callback_async(match))
return partial(self._subscriptions.discard, subscription)
@@ -139,7 +131,7 @@ class SupervisorJobs:
# If this is the first update register to receive Supervisor events
if first_update:
self._dispatcher_disconnect = async_dispatcher_connect(
async_dispatcher_connect(
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_jobs
)
@@ -166,14 +158,3 @@ class SupervisorJobs:
for sub in self._subscriptions:
if sub.matches(job):
sub.event_callback(job)
# If the job is done, pop it from our cache if present after processing is done
if job.done and job.uuid in self._jobs:
del self._jobs[job.uuid]
@callback
def unload(self) -> None:
"""Unregister with dispatcher on config entry unload."""
if self._dispatcher_disconnect:
self._dispatcher_disconnect()
self._dispatcher_disconnect = None

View File

@@ -75,7 +75,6 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
context: ConfigFlowContext
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
ZIGBEE_BAUDRATE = 460800
async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
@@ -113,6 +112,7 @@ class HomeAssistantConnectZBT2ConfigFlow(
VERSION = 1
MINOR_VERSION = 1
ZIGBEE_BAUDRATE = 460800
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the config flow."""

View File

@@ -1,6 +1,5 @@
"""Base class for IOmeter entities."""
from homeassistant.const import CONF_HOST
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -22,5 +21,4 @@ class IOmeterEntity(CoordinatorEntity[IOMeterCoordinator]):
manufacturer="IOmeter GmbH",
model="IOmeter",
sw_version=coordinator.current_fw_version,
configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}/",
)

View File

@@ -299,8 +299,8 @@ def _create_climate_ui(xknx: XKNX, conf: ConfigExtractor, name: str) -> XknxClim
group_address_active_state=conf.get_state_and_passive(CONF_GA_ACTIVE),
group_address_command_value_state=conf.get_state_and_passive(CONF_GA_VALVE),
sync_state=sync_state,
min_temp=conf.get(CONF_TARGET_TEMPERATURE, ClimateConf.MIN_TEMP),
max_temp=conf.get(CONF_TARGET_TEMPERATURE, ClimateConf.MAX_TEMP),
min_temp=conf.get(ClimateConf.MIN_TEMP),
max_temp=conf.get(ClimateConf.MAX_TEMP),
mode=climate_mode,
group_address_fan_speed=conf.get_write(CONF_GA_FAN_SPEED),
group_address_fan_speed_state=conf.get_state_and_passive(CONF_GA_FAN_SPEED),
@@ -486,7 +486,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
ha_controller_modes.append(self._last_hvac_mode)
ha_controller_modes.append(HVACMode.OFF)
hvac_modes = sorted(set(filter(None, ha_controller_modes)))
hvac_modes = list(set(filter(None, ha_controller_modes)))
return (
hvac_modes
if hvac_modes

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.10.0",
"xknxproject==3.8.2",
"knx-frontend==2025.10.31.195356"
"knx-frontend==2025.10.26.81530"
],
"single_config_entry": true
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["librehardwaremonitor-api==1.5.0"]
"requirements": ["librehardwaremonitor-api==1.4.0"]
}

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Any
from librehardwaremonitor_api.model import LibreHardwareMonitorSensorData
from homeassistant.components.sensor import SensorEntity, SensorStateClass
@@ -53,10 +51,10 @@ class LibreHardwareMonitorSensor(
super().__init__(coordinator)
self._attr_name: str = sensor_data.name
self._attr_native_value: str | None = sensor_data.value
self._attr_extra_state_attributes: dict[str, Any] = {
STATE_MIN_VALUE: sensor_data.min,
STATE_MAX_VALUE: sensor_data.max,
self.value: str | None = sensor_data.value
self._attr_extra_state_attributes: dict[str, str] = {
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
}
self._attr_native_unit_of_measurement = sensor_data.unit
self._attr_unique_id: str = f"{entry_id}_{sensor_data.sensor_id}"
@@ -74,12 +72,23 @@ class LibreHardwareMonitorSensor(
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if sensor_data := self.coordinator.data.sensor_data.get(self._sensor_id):
self._attr_native_value = sensor_data.value
self.value = sensor_data.value
self._attr_extra_state_attributes = {
STATE_MIN_VALUE: sensor_data.min,
STATE_MAX_VALUE: sensor_data.max,
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
}
else:
self._attr_native_value = None
self.value = None
super()._handle_coordinator_update()
@property
def native_value(self) -> str | None:
"""Return the formatted sensor value or None if no value is available."""
if self.value is not None and self.value != "-":
return self._format_number_value(self.value)
return None
@staticmethod
def _format_number_value(number_str: str) -> str:
return number_str.replace(",", ".")

View File

@@ -19,6 +19,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
STATE_UNKNOWN,
EntityCategory,
UnitOfEnergy,
UnitOfTemperature,
@@ -761,35 +762,40 @@ class MieleSensor(MieleEntity, SensorEntity):
class MieleRestorableSensor(MieleSensor, RestoreSensor):
"""Representation of a Sensor whose internal state can be restored."""
_attr_native_value: StateType
_last_value: StateType
def __init__(
self,
coordinator: MieleDataUpdateCoordinator,
device_id: str,
description: MieleSensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, device_id, description)
self._last_value = None
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# recover last value from cache when adding entity
last_data = await self.async_get_last_sensor_data()
if last_data:
self._attr_native_value = last_data.native_value # type: ignore[assignment]
last_value = await self.async_get_last_state()
if last_value and last_value.state != STATE_UNKNOWN:
self._last_value = last_value.state
@property
def native_value(self) -> StateType:
"""Return the state of the sensor.
"""Return the state of the sensor."""
return self._last_value
It is necessary to override `native_value` to fall back to the default
attribute-based implementation, instead of the function-based
implementation in `MieleSensor`.
"""
return self._attr_native_value
def _update_native_value(self) -> None:
"""Update the native value attribute of the sensor."""
self._attr_native_value = self.entity_description.value_fn(self.device)
def _update_last_value(self) -> None:
"""Update the last value of the sensor."""
self._last_value = self.entity_description.value_fn(self.device)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_native_value()
self._update_last_value()
super()._handle_coordinator_update()
@@ -906,7 +912,7 @@ class MieleProgramIdSensor(MieleSensor):
class MieleTimeSensor(MieleRestorableSensor):
"""Representation of time sensors keeping state from cache."""
def _update_native_value(self) -> None:
def _update_last_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
@@ -917,9 +923,7 @@ class MieleTimeSensor(MieleRestorableSensor):
current_status == StateStatus.PROGRAM_ENDED
and self.entity_description.end_value_fn is not None
):
self._attr_native_value = self.entity_description.end_value_fn(
self._attr_native_value
)
self._last_value = self.entity_description.end_value_fn(self._last_value)
# keep value when program ends if no function is specified
elif current_status == StateStatus.PROGRAM_ENDED:
@@ -927,11 +931,11 @@ class MieleTimeSensor(MieleRestorableSensor):
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
self._attr_native_value = None
self._last_value = None
# otherwise, cache value and return it
else:
self._attr_native_value = current_value
self._last_value = current_value
class MieleConsumptionSensor(MieleRestorableSensor):
@@ -939,13 +943,13 @@ class MieleConsumptionSensor(MieleRestorableSensor):
_is_reporting: bool = False
def _update_native_value(self) -> None:
def _update_last_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
last_value = (
float(cast(str, self._attr_native_value))
if self._attr_native_value is not None
float(cast(str, self._last_value))
if self._last_value is not None and self._last_value != STATE_UNKNOWN
else 0
)
@@ -959,7 +963,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
StateStatus.SERVICE,
):
self._is_reporting = False
self._attr_native_value = None
self._last_value = None
# appliance might report the last value for consumption of previous cycle and it will report 0
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
@@ -969,7 +973,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
and not self._is_reporting
and last_value > 0
):
self._attr_native_value = current_value
self._last_value = current_value
self._is_reporting = True
elif (
@@ -978,12 +982,12 @@ class MieleConsumptionSensor(MieleRestorableSensor):
and current_value is not None
and cast(int, current_value) > 0
):
self._attr_native_value = 0
self._last_value = 0
# keep value when program ends
elif current_status == StateStatus.PROGRAM_ENDED:
pass
else:
self._attr_native_value = current_value
self._last_value = current_value
self._is_reporting = True

View File

@@ -1,76 +0,0 @@
"""Support for Neato botvac connected vacuum cleaners."""
import logging
import aiohttp
from pybotvac import Account
from pybotvac.exceptions import NeatoException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from . import api
from .const import NEATO_DOMAIN, NEATO_LOGIN
from .hub import NeatoHub
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BUTTON,
Platform.CAMERA,
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up config entry."""
hass.data.setdefault(NEATO_DOMAIN, {})
if CONF_TOKEN not in entry.data:
raise ConfigEntryAuthFailed
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except aiohttp.ClientResponseError as ex:
_LOGGER.debug("API error: %s (%s)", ex.code, ex.message)
if ex.code in (401, 403):
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
raise ConfigEntryNotReady from ex
neato_session = api.ConfigEntryAuth(hass, entry, implementation)
hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session
hub = NeatoHub(hass, Account(neato_session))
await hub.async_update_entry_unique_id(entry)
try:
await hass.async_add_executor_job(hub.update_robots)
except NeatoException as ex:
_LOGGER.debug("Failed to connect to Neato API")
raise ConfigEntryNotReady from ex
hass.data[NEATO_LOGIN] = hub
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[NEATO_DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -1,58 +0,0 @@
"""API for Neato Botvac bound to Home Assistant OAuth."""
from __future__ import annotations
from asyncio import run_coroutine_threadsafe
from typing import Any
import pybotvac
from homeassistant import config_entries, core
from homeassistant.components.application_credentials import AuthImplementation
from homeassistant.helpers import config_entry_oauth2_flow
class ConfigEntryAuth(pybotvac.OAuthSession): # type: ignore[misc]
"""Provide Neato Botvac authentication tied to an OAuth2 based config entry."""
def __init__(
self,
hass: core.HomeAssistant,
config_entry: config_entries.ConfigEntry,
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
) -> None:
"""Initialize Neato Botvac Auth."""
self.hass = hass
self.session = config_entry_oauth2_flow.OAuth2Session(
hass, config_entry, implementation
)
super().__init__(self.session.token, vendor=pybotvac.Neato())
def refresh_tokens(self) -> str:
"""Refresh and return new Neato Botvac tokens."""
run_coroutine_threadsafe(
self.session.async_ensure_token_valid(), self.hass.loop
).result()
return self.session.token["access_token"] # type: ignore[no-any-return]
class NeatoImplementation(AuthImplementation):
"""Neato implementation of LocalOAuth2Implementation.
We need this class because we have to add client_secret
and scope to the authorization request.
"""
@property
def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
return {"client_secret": self.client_secret}
async def async_generate_authorize_url(self, flow_id: str) -> str:
"""Generate a url for the user to authorize.
We must make sure that the plus signs are not encoded.
"""
url = await super().async_generate_authorize_url(flow_id)
return f"{url}&scope=public_profile+control_robots+maps"

View File

@@ -1,28 +0,0 @@
"""Application credentials platform for neato."""
from pybotvac import Neato
from homeassistant.components.application_credentials import (
AuthorizationServer,
ClientCredential,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from . import api
async def async_get_auth_implementation(
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
"""Return auth implementation for a custom auth implementation."""
vendor = Neato()
return api.NeatoImplementation(
hass,
auth_domain,
credential,
AuthorizationServer(
authorize_url=vendor.auth_endpoint,
token_url=vendor.token_endpoint,
),
)

View File

@@ -1,44 +0,0 @@
"""Support for Neato buttons."""
from __future__ import annotations
from pybotvac import Robot
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import NEATO_ROBOTS
from .entity import NeatoEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Neato button from config entry."""
entities = [NeatoDismissAlertButton(robot) for robot in hass.data[NEATO_ROBOTS]]
async_add_entities(entities, True)
class NeatoDismissAlertButton(NeatoEntity, ButtonEntity):
"""Representation of a dismiss_alert button entity."""
_attr_translation_key = "dismiss_alert"
_attr_entity_category = EntityCategory.CONFIG
def __init__(
self,
robot: Robot,
) -> None:
"""Initialize a dismiss_alert Neato button entity."""
super().__init__(robot)
self._attr_unique_id = f"{robot.serial}_dismiss_alert"
async def async_press(self) -> None:
"""Press the button."""
await self.hass.async_add_executor_job(self.robot.dismiss_current_alert)

View File

@@ -1,130 +0,0 @@
"""Support for loading picture from Neato."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from pybotvac.exceptions import NeatoRobotException
from pybotvac.robot import Robot
from urllib3.response import HTTPResponse
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES
from .entity import NeatoEntity
from .hub import NeatoHub
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
ATTR_GENERATED_AT = "generated_at"
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Neato camera with config entry."""
neato: NeatoHub = hass.data[NEATO_LOGIN]
mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA)
dev = [
NeatoCleaningMap(neato, robot, mapdata)
for robot in hass.data[NEATO_ROBOTS]
if "maps" in robot.traits
]
if not dev:
return
_LOGGER.debug("Adding robots for cleaning maps %s", dev)
async_add_entities(dev, True)
class NeatoCleaningMap(NeatoEntity, Camera):
"""Neato cleaning map for last clean."""
_attr_translation_key = "cleaning_map"
def __init__(
self, neato: NeatoHub, robot: Robot, mapdata: dict[str, Any] | None
) -> None:
"""Initialize Neato cleaning map."""
super().__init__(robot)
Camera.__init__(self)
self.neato = neato
self._mapdata = mapdata
self._available = neato is not None
self._robot_serial: str = self.robot.serial
self._attr_unique_id = self.robot.serial
self._generated_at: str | None = None
self._image_url: str | None = None
self._image: bytes | None = None
def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response."""
self.update()
return self._image
def update(self) -> None:
"""Check the contents of the map list."""
_LOGGER.debug("Running camera update for '%s'", self.entity_id)
try:
self.neato.update_robots()
except NeatoRobotException as ex:
if self._available: # Print only once when available
_LOGGER.error(
"Neato camera connection error for '%s': %s", self.entity_id, ex
)
self._image = None
self._image_url = None
self._available = False
return
if self._mapdata:
map_data: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0]
if (image_url := map_data["url"]) == self._image_url:
_LOGGER.debug(
"The map image_url for '%s' is the same as old", self.entity_id
)
return
try:
image: HTTPResponse = self.neato.download_map(image_url)
except NeatoRobotException as ex:
if self._available: # Print only once when available
_LOGGER.error(
"Neato camera connection error for '%s': %s", self.entity_id, ex
)
self._image = None
self._image_url = None
self._available = False
return
self._image = image.read()
self._image_url = image_url
self._generated_at = map_data.get("generated_at")
self._available = True
@property
def available(self) -> bool:
"""Return if the robot is available."""
return self._available
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the vacuum cleaner."""
data: dict[str, Any] = {}
if self._generated_at is not None:
data[ATTR_GENERATED_AT] = self._generated_at
return data

View File

@@ -1,64 +0,0 @@
"""Config flow for Neato Botvac."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import NEATO_DOMAIN
class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=NEATO_DOMAIN
):
"""Config flow to handle Neato Botvac OAuth2 authentication."""
DOMAIN = NEATO_DOMAIN
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Create an entry for the flow."""
current_entries = self._async_current_entries()
if self.source != SOURCE_REAUTH and current_entries:
# Already configured
return self.async_abort(reason="already_configured")
return await super().async_step_user(user_input=user_input)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon migration of old entries."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth upon migration of old entries."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow. Update an entry if one already exist."""
current_entries = self._async_current_entries()
if self.source == SOURCE_REAUTH and current_entries:
# Update entry
self.hass.config_entries.async_update_entry(
current_entries[0], title=self.flow_impl.name, data=data
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(current_entries[0].entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self.flow_impl.name, data=data)

View File

@@ -1,150 +0,0 @@
"""Constants for Neato integration."""
NEATO_DOMAIN = "neato"
CONF_VENDOR = "vendor"
NEATO_LOGIN = "neato_login"
NEATO_MAP_DATA = "neato_map_data"
NEATO_PERSISTENT_MAPS = "neato_persistent_maps"
NEATO_ROBOTS = "neato_robots"
SCAN_INTERVAL_MINUTES = 1
MODE = {1: "Eco", 2: "Turbo"}
ACTION = {
0: "Invalid",
1: "House Cleaning",
2: "Spot Cleaning",
3: "Manual Cleaning",
4: "Docking",
5: "User Menu Active",
6: "Suspended Cleaning",
7: "Updating",
8: "Copying logs",
9: "Recovering Location",
10: "IEC test",
11: "Map cleaning",
12: "Exploring map (creating a persistent map)",
13: "Acquiring Persistent Map IDs",
14: "Creating & Uploading Map",
15: "Suspended Exploration",
}
ERRORS = {
"ui_error_battery_battundervoltlithiumsafety": "Replace battery",
"ui_error_battery_critical": "Replace battery",
"ui_error_battery_invalidsensor": "Replace battery",
"ui_error_battery_lithiumadapterfailure": "Replace battery",
"ui_error_battery_mismatch": "Replace battery",
"ui_error_battery_nothermistor": "Replace battery",
"ui_error_battery_overtemp": "Replace battery",
"ui_error_battery_overvolt": "Replace battery",
"ui_error_battery_undercurrent": "Replace battery",
"ui_error_battery_undertemp": "Replace battery",
"ui_error_battery_undervolt": "Replace battery",
"ui_error_battery_unplugged": "Replace battery",
"ui_error_brush_stuck": "Brush stuck",
"ui_error_brush_overloaded": "Brush overloaded",
"ui_error_bumper_stuck": "Bumper stuck",
"ui_error_check_battery_switch": "Check battery",
"ui_error_corrupt_scb": "Call customer service corrupt board",
"ui_error_deck_debris": "Deck debris",
"ui_error_dflt_app": "Check Neato app",
"ui_error_disconnect_chrg_cable": "Disconnected charge cable",
"ui_error_disconnect_usb_cable": "Disconnected USB cable",
"ui_error_dust_bin_missing": "Dust bin missing",
"ui_error_dust_bin_full": "Dust bin full",
"ui_error_dust_bin_emptied": "Dust bin emptied",
"ui_error_hardware_failure": "Hardware failure",
"ui_error_ldrop_stuck": "Clear my path",
"ui_error_lds_jammed": "Clear my path",
"ui_error_lds_bad_packets": "Check Neato app",
"ui_error_lds_disconnected": "Check Neato app",
"ui_error_lds_missed_packets": "Check Neato app",
"ui_error_lwheel_stuck": "Clear my path",
"ui_error_navigation_backdrop_frontbump": "Clear my path",
"ui_error_navigation_backdrop_leftbump": "Clear my path",
"ui_error_navigation_backdrop_wheelextended": "Clear my path",
"ui_error_navigation_noprogress": "Clear my path",
"ui_error_navigation_origin_unclean": "Clear my path",
"ui_error_navigation_pathproblems": "Cannot return to base",
"ui_error_navigation_pinkycommsfail": "Clear my path",
"ui_error_navigation_falling": "Clear my path",
"ui_error_navigation_noexitstogo": "Clear my path",
"ui_error_navigation_nomotioncommands": "Clear my path",
"ui_error_navigation_rightdrop_leftbump": "Clear my path",
"ui_error_navigation_undockingfailed": "Clear my path",
"ui_error_picked_up": "Picked up",
"ui_error_qa_fail": "Check Neato app",
"ui_error_rdrop_stuck": "Clear my path",
"ui_error_reconnect_failed": "Reconnect failed",
"ui_error_rwheel_stuck": "Clear my path",
"ui_error_stuck": "Stuck!",
"ui_error_unable_to_return_to_base": "Unable to return to base",
"ui_error_unable_to_see": "Clean vacuum sensors",
"ui_error_vacuum_slip": "Clear my path",
"ui_error_vacuum_stuck": "Clear my path",
"ui_error_warning": "Error check app",
"batt_base_connect_fail": "Battery failed to connect to base",
"batt_base_no_power": "Battery base has no power",
"batt_low": "Battery low",
"batt_on_base": "Battery on base",
"clean_tilt_on_start": "Clean the tilt on start",
"dustbin_full": "Dust bin full",
"dustbin_missing": "Dust bin missing",
"gen_picked_up": "Picked up",
"hw_fail": "Hardware failure",
"hw_tof_sensor_sensor": "Hardware sensor disconnected",
"lds_bad_packets": "Bad packets",
"lds_deck_debris": "Debris on deck",
"lds_disconnected": "Disconnected",
"lds_jammed": "Jammed",
"lds_missed_packets": "Missed packets",
"maint_brush_stuck": "Brush stuck",
"maint_brush_overload": "Brush overloaded",
"maint_bumper_stuck": "Bumper stuck",
"maint_customer_support_qa": "Contact customer support",
"maint_vacuum_stuck": "Vacuum is stuck",
"maint_vacuum_slip": "Vacuum is stuck",
"maint_left_drop_stuck": "Vacuum is stuck",
"maint_left_wheel_stuck": "Vacuum is stuck",
"maint_right_drop_stuck": "Vacuum is stuck",
"maint_right_wheel_stuck": "Vacuum is stuck",
"not_on_charge_base": "Not on the charge base",
"nav_robot_falling": "Clear my path",
"nav_no_path": "Clear my path",
"nav_path_problem": "Clear my path",
"nav_backdrop_frontbump": "Clear my path",
"nav_backdrop_leftbump": "Clear my path",
"nav_backdrop_wheelextended": "Clear my path",
"nav_floorplan_zone_path_blocked": "Clear my path",
"nav_mag_sensor": "Clear my path",
"nav_no_exit": "Clear my path",
"nav_no_movement": "Clear my path",
"nav_rightdrop_leftbump": "Clear my path",
"nav_undocking_failed": "Clear my path",
}
ALERTS = {
"ui_alert_dust_bin_full": "Please empty dust bin",
"ui_alert_recovering_location": "Returning to start",
"ui_alert_battery_chargebasecommerr": "Battery error",
"ui_alert_busy_charging": "Busy charging",
"ui_alert_charging_base": "Base charging",
"ui_alert_charging_power": "Charging power",
"ui_alert_connect_chrg_cable": "Connect charge cable",
"ui_alert_info_thank_you": "Thank you",
"ui_alert_invalid": "Invalid check app",
"ui_alert_old_error": "Old error",
"ui_alert_swupdate_fail": "Update failed",
"dustbin_full": "Please empty dust bin",
"maint_brush_change": "Change the brush",
"maint_filter_change": "Change the filter",
"clean_completed_to_start": "Cleaning completed",
"nav_floorplan_not_created": "No floorplan found",
"nav_floorplan_load_fail": "Failed to load floorplan",
"nav_floorplan_localization_fail": "Failed to load floorplan",
"clean_incomplete_to_start": "Cleaning incomplete",
"log_upload_failed": "Logs failed to upload",
}

View File

@@ -1,24 +0,0 @@
"""Base entity for Neato."""
from __future__ import annotations
from pybotvac import Robot
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import NEATO_DOMAIN
class NeatoEntity(Entity):
"""Base Neato entity."""
_attr_has_entity_name = True
def __init__(self, robot: Robot) -> None:
"""Initialize Neato entity."""
self.robot = robot
self._attr_device_info: DeviceInfo = DeviceInfo(
identifiers={(NEATO_DOMAIN, self.robot.serial)},
name=self.robot.name,
)

View File

@@ -1,50 +0,0 @@
"""Support for Neato botvac connected vacuum cleaners."""
from datetime import timedelta
import logging
from pybotvac import Account
from urllib3.response import HTTPResponse
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.util import Throttle
from .const import NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS
_LOGGER = logging.getLogger(__name__)
class NeatoHub:
"""A My Neato hub wrapper class."""
def __init__(self, hass: HomeAssistant, neato: Account) -> None:
"""Initialize the Neato hub."""
self._hass = hass
self.my_neato: Account = neato
@Throttle(timedelta(minutes=1))
def update_robots(self) -> None:
"""Update the robot states."""
_LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS))
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
def download_map(self, url: str) -> HTTPResponse:
"""Download a new map image."""
map_image_data: HTTPResponse = self.my_neato.get_map_image(url)
return map_image_data
async def async_update_entry_unique_id(self, entry: ConfigEntry) -> str:
"""Update entry for unique_id."""
await self._hass.async_add_executor_job(self.my_neato.refresh_userdata)
unique_id: str = self.my_neato.unique_id
if entry.unique_id == unique_id:
return unique_id
_LOGGER.debug("Updating user unique_id for previous config entry")
self._hass.config_entries.async_update_entry(entry, unique_id=unique_id)
return unique_id

View File

@@ -1,7 +0,0 @@
{
"services": {
"custom_cleaning": {
"service": "mdi:broom"
}
}
}

View File

@@ -1,11 +0,0 @@
{
"domain": "neato",
"name": "Neato Botvac",
"codeowners": [],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/neato",
"iot_class": "cloud_polling",
"loggers": ["pybotvac"],
"requirements": ["pybotvac==0.0.28"]
}

View File

@@ -1,81 +0,0 @@
"""Support for Neato sensors."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from pybotvac.exceptions import NeatoRobotException
from pybotvac.robot import Robot
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES
from .entity import NeatoEntity
from .hub import NeatoHub
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
BATTERY = "Battery"
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Neato sensor using config entry."""
neato: NeatoHub = hass.data[NEATO_LOGIN]
dev = [NeatoSensor(neato, robot) for robot in hass.data[NEATO_ROBOTS]]
if not dev:
return
_LOGGER.debug("Adding robots for sensors %s", dev)
async_add_entities(dev, True)
class NeatoSensor(NeatoEntity, SensorEntity):
"""Neato sensor."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = PERCENTAGE
_attr_available: bool = False
def __init__(self, neato: NeatoHub, robot: Robot) -> None:
"""Initialize Neato sensor."""
super().__init__(robot)
self._robot_serial: str = self.robot.serial
self._attr_unique_id = self.robot.serial
self._state: dict[str, Any] | None = None
def update(self) -> None:
"""Update Neato Sensor."""
try:
self._state = self.robot.state
except NeatoRobotException as ex:
if self._attr_available:
_LOGGER.error(
"Neato sensor connection error for '%s': %s", self.entity_id, ex
)
self._state = None
self._attr_available = False
return
self._attr_available = True
_LOGGER.debug("self._state=%s", self._state)
@property
def native_value(self) -> str | None:
"""Return the state."""
if self._state is not None:
return str(self._state["details"]["charge"])
return None

View File

@@ -1,32 +0,0 @@
custom_cleaning:
target:
entity:
integration: neato
domain: vacuum
fields:
mode:
default: 2
selector:
number:
min: 1
max: 2
mode: box
navigation:
default: 1
selector:
number:
min: 1
max: 3
mode: box
category:
default: 4
selector:
number:
min: 2
max: 4
step: 2
mode: box
zone:
example: "Kitchen"
selector:
text:

View File

@@ -1,73 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"step": {
"pick_implementation": {
"data": {
"implementation": "[%key:common::config_flow::data::implementation%]"
},
"data_description": {
"implementation": "[%key:common::config_flow::description::implementation%]"
},
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::description::confirm_setup%]"
}
}
},
"entity": {
"button": {
"dismiss_alert": {
"name": "Dismiss alert"
}
},
"camera": {
"cleaning_map": {
"name": "Cleaning map"
}
},
"switch": {
"schedule": {
"name": "Schedule"
}
}
},
"services": {
"custom_cleaning": {
"description": "Starts a custom cleaning of your house.",
"fields": {
"category": {
"description": "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found).",
"name": "Use cleaning map"
},
"mode": {
"description": "Sets the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set.",
"name": "Cleaning mode"
},
"navigation": {
"description": "Sets the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set.",
"name": "Navigation mode"
},
"zone": {
"description": "Name of the zone to clean (only supported on the Botvac D7). Defaults to no zone i.e. complete house cleanup.",
"name": "Zone"
}
},
"name": "Custom cleaning"
}
}
}

View File

@@ -1,118 +0,0 @@
"""Support for Neato Connected Vacuums switches."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from pybotvac.exceptions import NeatoRobotException
from pybotvac.robot import Robot
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES
from .entity import NeatoEntity
from .hub import NeatoHub
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
SWITCH_TYPE_SCHEDULE = "schedule"
SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Neato switch with config entry."""
neato: NeatoHub = hass.data[NEATO_LOGIN]
dev = [
NeatoConnectedSwitch(neato, robot, type_name)
for robot in hass.data[NEATO_ROBOTS]
for type_name in SWITCH_TYPES
]
if not dev:
return
_LOGGER.debug("Adding switches %s", dev)
async_add_entities(dev, True)
class NeatoConnectedSwitch(NeatoEntity, SwitchEntity):
"""Neato Connected Switches."""
_attr_translation_key = "schedule"
_attr_available = False
_attr_entity_category = EntityCategory.CONFIG
def __init__(self, neato: NeatoHub, robot: Robot, switch_type: str) -> None:
"""Initialize the Neato Connected switches."""
super().__init__(robot)
self.type = switch_type
self._state: dict[str, Any] | None = None
self._schedule_state: str | None = None
self._clean_state = None
self._attr_unique_id = self.robot.serial
def update(self) -> None:
"""Update the states of Neato switches."""
_LOGGER.debug("Running Neato switch update for '%s'", self.entity_id)
try:
self._state = self.robot.state
except NeatoRobotException as ex:
if self._attr_available: # Print only once when available
_LOGGER.error(
"Neato switch connection error for '%s': %s", self.entity_id, ex
)
self._state = None
self._attr_available = False
return
self._attr_available = True
_LOGGER.debug("self._state=%s", self._state)
if self.type == SWITCH_TYPE_SCHEDULE:
_LOGGER.debug("State: %s", self._state)
if self._state is not None and self._state["details"]["isScheduleEnabled"]:
self._schedule_state = STATE_ON
else:
self._schedule_state = STATE_OFF
_LOGGER.debug(
"Schedule state for '%s': %s", self.entity_id, self._schedule_state
)
@property
def is_on(self) -> bool:
"""Return true if switch is on."""
return bool(
self.type == SWITCH_TYPE_SCHEDULE and self._schedule_state == STATE_ON
)
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
if self.type == SWITCH_TYPE_SCHEDULE:
try:
self.robot.enable_schedule()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato switch connection error '%s': %s", self.entity_id, ex
)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
if self.type == SWITCH_TYPE_SCHEDULE:
try:
self.robot.disable_schedule()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato switch connection error '%s': %s", self.entity_id, ex
)

View File

@@ -1,388 +0,0 @@
"""Support for Neato Connected Vacuums."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from pybotvac import Robot
from pybotvac.exceptions import NeatoRobotException
import voluptuous as vol
from homeassistant.components.vacuum import (
ATTR_STATUS,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
ACTION,
ALERTS,
ERRORS,
MODE,
NEATO_LOGIN,
NEATO_MAP_DATA,
NEATO_PERSISTENT_MAPS,
NEATO_ROBOTS,
SCAN_INTERVAL_MINUTES,
)
from .entity import NeatoEntity
from .hub import NeatoHub
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES)
ATTR_CLEAN_START = "clean_start"
ATTR_CLEAN_STOP = "clean_stop"
ATTR_CLEAN_AREA = "clean_area"
ATTR_CLEAN_BATTERY_START = "battery_level_at_clean_start"
ATTR_CLEAN_BATTERY_END = "battery_level_at_clean_end"
ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count"
ATTR_CLEAN_SUSP_TIME = "clean_suspension_time"
ATTR_CLEAN_PAUSE_TIME = "clean_pause_time"
ATTR_CLEAN_ERROR_TIME = "clean_error_time"
ATTR_LAUNCHED_FROM = "launched_from"
ATTR_NAVIGATION = "navigation"
ATTR_CATEGORY = "category"
ATTR_ZONE = "zone"
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Neato vacuum with config entry."""
neato: NeatoHub = hass.data[NEATO_LOGIN]
mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA)
persistent_maps: dict[str, Any] | None = hass.data.get(NEATO_PERSISTENT_MAPS)
dev = [
NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)
for robot in hass.data[NEATO_ROBOTS]
]
if not dev:
return
_LOGGER.debug("Adding vacuums %s", dev)
async_add_entities(dev, True)
platform = entity_platform.async_get_current_platform()
assert platform is not None
platform.async_register_entity_service(
"custom_cleaning",
{
vol.Optional(ATTR_MODE, default=2): cv.positive_int,
vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int,
vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int,
vol.Optional(ATTR_ZONE): cv.string,
},
"neato_custom_cleaning",
)
class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
"""Representation of a Neato Connected Vacuum."""
_attr_supported_features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.STOP
| VacuumEntityFeature.START
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.STATE
| VacuumEntityFeature.MAP
| VacuumEntityFeature.LOCATE
)
_attr_name = None
def __init__(
self,
neato: NeatoHub,
robot: Robot,
mapdata: dict[str, Any] | None,
persistent_maps: dict[str, Any] | None,
) -> None:
"""Initialize the Neato Connected Vacuum."""
super().__init__(robot)
self._attr_available: bool = neato is not None
self._mapdata = mapdata
self._robot_has_map: bool = self.robot.has_persistent_maps
self._robot_maps = persistent_maps
self._robot_serial: str = self.robot.serial
self._attr_unique_id: str = self.robot.serial
self._status_state: str | None = None
self._state: dict[str, Any] | None = None
self._clean_time_start: str | None = None
self._clean_time_stop: str | None = None
self._clean_area: float | None = None
self._clean_battery_start: int | None = None
self._clean_battery_end: int | None = None
self._clean_susp_charge_count: int | None = None
self._clean_susp_time: int | None = None
self._clean_pause_time: int | None = None
self._clean_error_time: int | None = None
self._launched_from: str | None = None
self._robot_boundaries: list = []
self._robot_stats: dict[str, Any] | None = None
def update(self) -> None:
"""Update the states of Neato Vacuums."""
_LOGGER.debug("Running Neato Vacuums update for '%s'", self.entity_id)
try:
if self._robot_stats is None:
self._robot_stats = self.robot.get_general_info().json().get("data")
except NeatoRobotException:
_LOGGER.warning("Couldn't fetch robot information of %s", self.entity_id)
try:
self._state = self.robot.state
except NeatoRobotException as ex:
if self._attr_available: # print only once when available
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
self._state = None
self._attr_available = False
return
if self._state is None:
return
self._attr_available = True
_LOGGER.debug("self._state=%s", self._state)
if "alert" in self._state:
robot_alert = ALERTS.get(self._state["alert"])
else:
robot_alert = None
if self._state["state"] == 1:
if self._state["details"]["isCharging"]:
self._attr_activity = VacuumActivity.DOCKED
self._status_state = "Charging"
elif (
self._state["details"]["isDocked"]
and not self._state["details"]["isCharging"]
):
self._attr_activity = VacuumActivity.DOCKED
self._status_state = "Docked"
else:
self._attr_activity = VacuumActivity.IDLE
self._status_state = "Stopped"
if robot_alert is not None:
self._status_state = robot_alert
elif self._state["state"] == 2:
if robot_alert is None:
self._attr_activity = VacuumActivity.CLEANING
self._status_state = (
f"{MODE.get(self._state['cleaning']['mode'])} "
f"{ACTION.get(self._state['action'])}"
)
if (
"boundary" in self._state["cleaning"]
and "name" in self._state["cleaning"]["boundary"]
):
self._status_state += (
f" {self._state['cleaning']['boundary']['name']}"
)
else:
self._status_state = robot_alert
elif self._state["state"] == 3:
self._attr_activity = VacuumActivity.PAUSED
self._status_state = "Paused"
elif self._state["state"] == 4:
self._attr_activity = VacuumActivity.ERROR
self._status_state = ERRORS.get(self._state["error"])
self._attr_battery_level = self._state["details"]["charge"]
if self._mapdata is None or not self._mapdata.get(self._robot_serial, {}).get(
"maps", []
):
return
mapdata: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0]
self._clean_time_start = mapdata["start_at"]
self._clean_time_stop = mapdata["end_at"]
self._clean_area = mapdata["cleaned_area"]
self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"]
self._clean_susp_time = mapdata["time_in_suspended_cleaning"]
self._clean_pause_time = mapdata["time_in_pause"]
self._clean_error_time = mapdata["time_in_error"]
self._clean_battery_start = mapdata["run_charge_at_start"]
self._clean_battery_end = mapdata["run_charge_at_end"]
self._launched_from = mapdata["launched_from"]
if (
self._robot_has_map
and self._state
and self._state["availableServices"]["maps"] != "basic-1"
and self._robot_maps
):
allmaps: dict = self._robot_maps[self._robot_serial]
_LOGGER.debug(
"Found the following maps for '%s': %s", self.entity_id, allmaps
)
self._robot_boundaries = [] # Reset boundaries before refreshing boundaries
for maps in allmaps:
try:
robot_boundaries = self.robot.get_map_boundaries(maps["id"]).json()
except NeatoRobotException as ex:
_LOGGER.error(
"Could not fetch map boundaries for '%s': %s",
self.entity_id,
ex,
)
return
_LOGGER.debug(
"Boundaries for robot '%s' in map '%s': %s",
self.entity_id,
maps["name"],
robot_boundaries,
)
if "boundaries" in robot_boundaries["data"]:
self._robot_boundaries += robot_boundaries["data"]["boundaries"]
_LOGGER.debug(
"List of boundaries for '%s': %s",
self.entity_id,
self._robot_boundaries,
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the vacuum cleaner."""
data: dict[str, Any] = {}
if self._status_state is not None:
data[ATTR_STATUS] = self._status_state
if self._clean_time_start is not None:
data[ATTR_CLEAN_START] = self._clean_time_start
if self._clean_time_stop is not None:
data[ATTR_CLEAN_STOP] = self._clean_time_stop
if self._clean_area is not None:
data[ATTR_CLEAN_AREA] = self._clean_area
if self._clean_susp_charge_count is not None:
data[ATTR_CLEAN_SUSP_COUNT] = self._clean_susp_charge_count
if self._clean_susp_time is not None:
data[ATTR_CLEAN_SUSP_TIME] = self._clean_susp_time
if self._clean_pause_time is not None:
data[ATTR_CLEAN_PAUSE_TIME] = self._clean_pause_time
if self._clean_error_time is not None:
data[ATTR_CLEAN_ERROR_TIME] = self._clean_error_time
if self._clean_battery_start is not None:
data[ATTR_CLEAN_BATTERY_START] = self._clean_battery_start
if self._clean_battery_end is not None:
data[ATTR_CLEAN_BATTERY_END] = self._clean_battery_end
if self._launched_from is not None:
data[ATTR_LAUNCHED_FROM] = self._launched_from
return data
@property
def device_info(self) -> DeviceInfo:
"""Device info for neato robot."""
device_info = self._attr_device_info
if self._robot_stats:
device_info["manufacturer"] = self._robot_stats["battery"]["vendor"]
device_info["model"] = self._robot_stats["model"]
device_info["sw_version"] = self._robot_stats["firmware"]
return device_info
def start(self) -> None:
"""Start cleaning or resume cleaning."""
if self._state:
try:
if self._state["state"] == 1:
self.robot.start_cleaning()
elif self._state["state"] == 3:
self.robot.resume_cleaning()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
def pause(self) -> None:
"""Pause the vacuum."""
try:
self.robot.pause_cleaning()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
def return_to_base(self, **kwargs: Any) -> None:
"""Set the vacuum cleaner to return to the dock."""
try:
if self._attr_activity == VacuumActivity.CLEANING:
self.robot.pause_cleaning()
self._attr_activity = VacuumActivity.RETURNING
self.robot.send_to_base()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
def stop(self, **kwargs: Any) -> None:
"""Stop the vacuum cleaner."""
try:
self.robot.stop_cleaning()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
def locate(self, **kwargs: Any) -> None:
"""Locate the robot by making it emit a sound."""
try:
self.robot.locate()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
def clean_spot(self, **kwargs: Any) -> None:
"""Run a spot cleaning starting from the base."""
try:
self.robot.start_spot_cleaning()
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
def neato_custom_cleaning(
self, mode: str, navigation: str, category: str, zone: str | None = None
) -> None:
"""Zone cleaning service call."""
boundary_id = None
if zone is not None:
for boundary in self._robot_boundaries:
if zone in boundary["name"]:
boundary_id = boundary["id"]
if boundary_id is None:
_LOGGER.error(
"Zone '%s' was not found for the robot '%s'", zone, self.entity_id
)
return
_LOGGER.debug(
"Start cleaning zone '%s' with robot %s", zone, self.entity_id
)
self._attr_activity = VacuumActivity.CLEANING
try:
self.robot.start_cleaning(mode, navigation, category, boundary_id)
except NeatoRobotException as ex:
_LOGGER.error(
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)

View File

@@ -7,7 +7,6 @@ import logging
from typing import TYPE_CHECKING, Any
from pynintendoparental import Authenticator
from pynintendoparental.api import Api
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
import voluptuous as vol
@@ -15,7 +14,7 @@ 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 APP_SETUP_URL, CONF_SESSION_TOKEN, DOMAIN
from .const import CONF_SESSION_TOKEN, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -38,9 +37,6 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
)
if user_input is not None:
nintendo_api = Api(
self.auth, self.hass.config.time_zone, self.hass.config.language
)
try:
await self.auth.complete_login(
self.auth, user_input[CONF_API_TOKEN], False
@@ -52,24 +48,12 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
assert self.auth.account_id
await self.async_set_unique_id(self.auth.account_id)
self._abort_if_unique_id_configured()
try:
if "base" not in errors:
await nintendo_api.async_get_account_devices()
except HttpException as err:
if err.status_code == 404:
return self.async_abort(
reason="no_devices_found",
description_placeholders={"more_info_url": APP_SETUP_URL},
)
errors["base"] = "cannot_connect"
else:
if "base" not in errors:
return self.async_create_entry(
title=self.auth.account_id,
data={
CONF_SESSION_TOKEN: self.auth.get_session_token,
},
)
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},

View File

@@ -8,7 +8,4 @@ BEDTIME_ALARM_MIN = "16:00"
BEDTIME_ALARM_MAX = "23:00"
BEDTIME_ALARM_DISABLE = "00:00"
APP_SETUP_URL = (
"https://www.nintendo.com/my/support/switch/parentalcontrols/app/setup.html"
)
ATTR_BONUS_TIME = "bonus_time"

View File

@@ -6,10 +6,7 @@ from datetime import timedelta
import logging
from pynintendoparental import Authenticator, NintendoParental
from pynintendoparental.exceptions import (
InvalidOAuthConfigurationException,
NoDevicesFoundException,
)
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -27,8 +24,6 @@ UPDATE_INTERVAL = timedelta(seconds=60)
class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Nintendo data update coordinator."""
config_entry: NintendoParentalControlsConfigEntry
def __init__(
self,
hass: HomeAssistant,
@@ -55,8 +50,3 @@ class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
raise ConfigEntryError(
err, translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except NoDevicesFoundException as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="no_devices_found",
) from err

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoparental==1.1.3"]
"requirements": ["pynintendoparental==1.1.2"]
}

View File

@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"no_devices_found": "There are no devices paired with this Nintendo account, go to [Nintendo Support]({more_info_url}) for further assistance.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
@@ -68,9 +67,6 @@
},
"device_not_found": {
"message": "Device not found."
},
"no_devices_found": {
"message": "No Nintendo devices found for this account."
}
},
"services": {

View File

@@ -14,7 +14,7 @@ from onedrive_personal_sdk.exceptions import (
NotFoundError,
OneDriveException,
)
from onedrive_personal_sdk.models.items import ItemUpdate
from onedrive_personal_sdk.models.items import Item, ItemUpdate
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
@@ -202,7 +202,9 @@ async def _get_onedrive_client(
)
async def _handle_item_operation[T](func: Callable[[], Awaitable[T]], folder: str) -> T:
async def _handle_item_operation(
func: Callable[[], Awaitable[Item]], folder: str
) -> Item:
try:
return await func()
except NotFoundError:

View File

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

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["openai==2.2.0", "python-open-router==0.3.2"]
"requirements": ["openai==2.2.0", "python-open-router==0.3.1"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.15.9"]
"requirements": ["opower==0.15.8"]
}

View File

@@ -229,7 +229,7 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the local authentication step via config flow."""
errors = {}
description_placeholders = {
"somfy_developer_mode_docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started"
"somfy-developer-mode-docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started"
}
if user_input:

View File

@@ -41,7 +41,7 @@
"token": "Token generated by the app used to control your device.",
"verify_ssl": "Verify the SSL certificate. Select this only if you are connecting via the hostname."
},
"description": "By activating the [Developer Mode of your TaHoma box]({somfy_developer_mode_docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway."
"description": "By activating the [Developer Mode of your TaHoma box]({somfy-developer-mode-docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway."
},
"local_or_cloud": {
"data": {

View File

@@ -38,7 +38,9 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
client = Portainer(
api_url=data[CONF_URL],
api_key=data[CONF_API_TOKEN],
session=async_get_clientsession(hass=hass, verify_ssl=data[CONF_VERIFY_SSL]),
session=async_get_clientsession(
hass=hass, verify_ssl=data.get(CONF_VERIFY_SSL, True)
),
)
try:
await client.get_endpoints()

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.12"]
"requirements": ["pyportainer==1.0.11"]
}

View File

@@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.16.4"]
"requirements": ["reolink-aio==0.16.2"]
}

View File

@@ -835,7 +835,6 @@
"vehicle_type": {
"name": "Vehicle type",
"state": {
"bus": "Bus",
"motorcycle": "Motorcycle",
"pickup_truck": "Pickup truck",
"sedan": "Sedan",

View File

@@ -417,7 +417,7 @@ def get_rpc_sub_device_name(
"""Get name based on device and channel name."""
if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if (zone_id := get_irrigation_zone_id(device, key)) is not None:
if (zone_id := get_irrigation_zone_id(device.config, key)) is not None:
# workaround for Irrigation controller, name stored in "service:0"
if zone_name := device.config["service:0"]["zones"][zone_id]["name"]:
return cast(str, zone_name)
@@ -792,13 +792,9 @@ async def get_rpc_scripts_event_types(
return script_events
def get_irrigation_zone_id(device: RpcDevice, key: str) -> int | None:
def get_irrigation_zone_id(config: dict[str, Any], key: str) -> int | None:
"""Return the zone id if the component is an irrigation zone."""
if (
device.initialized
and key in device.config
and (zone := get_rpc_role_by_key(device.config, key)).startswith("zone")
):
if key in config and (zone := get_rpc_role_by_key(config, key)).startswith("zone"):
return int(zone[4:])
return None
@@ -841,7 +837,7 @@ def get_rpc_device_info(
if (
(
component not in (*All_LIGHT_TYPES, "cover", "em1", "switch")
and get_irrigation_zone_id(device, key) is None
and get_irrigation_zone_id(device.config, key) is None
)
or idx is None
or len(get_rpc_key_instances(device.status, component, all_lights=True)) < 2

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/smarttub",
"iot_class": "cloud_polling",
"loggers": ["smarttub"],
"requirements": ["python-smarttub==0.0.45"]
"requirements": ["python-smarttub==0.0.44"]
}

View File

@@ -4,13 +4,11 @@ import logging
from libsoundtouch import soundtouch_device
from libsoundtouch.device import SoundTouchDevice
import requests
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -132,14 +130,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bose SoundTouch from a config entry."""
try:
device = await hass.async_add_executor_job(
soundtouch_device, entry.data[CONF_HOST]
)
except requests.exceptions.ConnectionError as err:
raise ConfigEntryNotReady(
f"Unable to connect to SoundTouch device at {entry.data[CONF_HOST]}"
) from err
device = await hass.async_add_executor_job(soundtouch_device, entry.data[CONF_HOST])
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device)

View File

@@ -24,8 +24,7 @@ from homeassistant.components.telegram_bot import (
)
from homeassistant.const import ATTR_LOCATION
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.reload import setup_reload_service
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, PLATFORMS
@@ -46,25 +45,14 @@ PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
)
async def async_get_service(
def get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> TelegramNotificationService:
"""Get the Telegram notification service."""
ir.async_create_issue(
hass,
DOMAIN,
"migrate_notify",
breaks_in_ha_version="2026.5.0",
is_fixable=False,
translation_key="migrate_notify",
severity=ir.IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/telegram_bot#notifiers",
)
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
setup_reload_service(hass, DOMAIN, PLATFORMS)
chat_id = config.get(CONF_CHAT_ID)
return TelegramNotificationService(hass, chat_id)

View File

@@ -1,10 +1,4 @@
{
"issues": {
"migrate_notify": {
"description": "The Telegram `notify` service has been migrated. A new `notify` entity per chat ID is available now.\n\nUpdate all affected automations to use the new `notify.send_message` action exposed by these new entities and then restart Home Assistant.",
"title": "Migration of Telegram notify service"
}
},
"services": {
"reload": {
"description": "Reloads telegram notify services.",

View File

@@ -108,8 +108,8 @@ from .const import (
SERVICE_SEND_STICKER,
SERVICE_SEND_VIDEO,
SERVICE_SEND_VOICE,
SIGNAL_UPDATE_EVENT,
)
from .helpers import signal
_FILE_TYPES = ("animation", "document", "photo", "sticker", "video", "voice")
_LOGGER = logging.getLogger(__name__)
@@ -169,7 +169,7 @@ class BaseTelegramBot:
_LOGGER.debug("Firing event %s: %s", event_type, event_data)
self.hass.bus.async_fire(event_type, event_data, context=event_context)
async_dispatcher_send(self.hass, signal(self._bot), event_type, event_data)
async_dispatcher_send(self.hass, SIGNAL_UPDATE_EVENT, event_type, event_data)
return True
@staticmethod
@@ -551,7 +551,7 @@ class TelegramNotificationService:
EVENT_TELEGRAM_SENT, event_data, context=context
)
async_dispatcher_send(
self.hass, signal(self.bot), EVENT_TELEGRAM_SENT, event_data
self.hass, SIGNAL_UPDATE_EVENT, EVENT_TELEGRAM_SENT, event_data
)
except TelegramError as exc:
if not suppress_error:

View File

@@ -14,9 +14,9 @@ from .const import (
EVENT_TELEGRAM_COMMAND,
EVENT_TELEGRAM_SENT,
EVENT_TELEGRAM_TEXT,
SIGNAL_UPDATE_EVENT,
)
from .entity import TelegramBotEntity
from .helpers import signal
async def async_setup_entry(
@@ -55,7 +55,7 @@ class TelegramBotEventEntity(TelegramBotEntity, EventEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
signal(self.config_entry.runtime_data.bot),
SIGNAL_UPDATE_EVENT,
self._async_handle_event,
)
)

View File

@@ -1,10 +0,0 @@
"""Helper functions for Telegram bot integration."""
from telegram import Bot
from .const import SIGNAL_UPDATE_EVENT
def signal(bot: Bot) -> str:
"""Define signal name."""
return f"{SIGNAL_UPDATE_EVENT}_{bot.id}"

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.2.5"]
"requirements": ["tesla-fleet-api==1.2.3"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.2.5", "teslemetry-stream==0.7.10"]
"requirements": ["tesla-fleet-api==1.2.3", "teslemetry-stream==0.7.10"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"],
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.5"]
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.3"]
}

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Sequence
import dataclasses
import fnmatch
import os
@@ -30,6 +29,15 @@ def usb_device_from_port(port: ListPortInfo) -> USBDevice:
def scan_serial_ports() -> Sequence[USBDevice]:
"""Scan serial ports for USB devices."""
return [
usb_device_from_port(port)
for port in comports()
if port.vid is not None or port.pid is not None
]
def usb_device_from_path(device_path: str) -> USBDevice | None:
"""Get USB device info from a device path."""
# Scan all symlinks first
by_id = "/dev/serial/by-id"
@@ -38,30 +46,23 @@ def scan_serial_ports() -> Sequence[USBDevice]:
for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
realpath_to_by_id[os.path.realpath(path)] = path
serial_ports = []
for port in comports():
if port.vid is not None or port.pid is not None:
usb_device = usb_device_from_port(port)
device_path = realpath_to_by_id.get(port.device, port.device)
if device_path != port.device:
# Prefer the unique /dev/serial/by-id/ path if it exists
usb_device = dataclasses.replace(usb_device, device=device_path)
serial_ports.append(usb_device)
return serial_ports
def usb_device_from_path(device_path: str) -> USBDevice | None:
"""Get USB device info from a device path."""
# Then compare the actual path to each serial port's
device_path_real = os.path.realpath(device_path)
for device in scan_serial_ports():
if os.path.realpath(device.device) == device_path_real:
return device
normalized_path = realpath_to_by_id.get(device.device, device.device)
if (
normalized_path == device_path
or os.path.realpath(device.device) == device_path_real
):
return USBDevice(
device=normalized_path,
vid=device.vid,
pid=device.pid,
serial_number=device.serial_number,
manufacturer=device.manufacturer,
description=device.description,
)
return None

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/vegehub",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["vegehub==0.1.26"],
"requirements": ["vegehub==0.1.24"],
"zeroconf": ["_vege._tcp.local."]
}

View File

@@ -267,12 +267,8 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
await self.async_set_preset_mode(preset_mode)
return
if percentage is None:
success = await self.device.turn_on()
if not success:
raise HomeAssistantError(self.device.last_response.message)
self.schedule_update_ha_state()
else:
await self.async_set_percentage(percentage)
percentage = 50
await self.async_set_percentage(percentage)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""

View File

@@ -37,7 +37,7 @@ _LOGGER = logging.getLogger(__name__)
VS_TO_HA_MODE_MAP = {
VS_HUMIDIFIER_MODE_AUTO: MODE_AUTO,
VS_HUMIDIFIER_MODE_HUMIDITY: VS_HUMIDIFIER_MODE_HUMIDITY,
VS_HUMIDIFIER_MODE_HUMIDITY: MODE_AUTO,
VS_HUMIDIFIER_MODE_MANUAL: MODE_NORMAL,
VS_HUMIDIFIER_MODE_SLEEP: MODE_SLEEP,
}
@@ -93,8 +93,6 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity):
_attr_supported_features = HumidifierEntityFeature.MODES
_attr_translation_key = "vesync"
def __init__(
self,
device: VeSyncBaseDevice,

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/vesync",
"iot_class": "cloud_polling",
"loggers": ["pyvesync"],
"requirements": ["pyvesync==3.1.4"]
"requirements": ["pyvesync==3.1.2"]
}

View File

@@ -46,7 +46,7 @@ NUMBER_DESCRIPTIONS: list[VeSyncNumberEntityDescription] = [
mode=NumberMode.SLIDER,
exists_fn=is_humidifier,
set_value_fn=lambda device, value: device.set_mist_level(value),
value_fn=lambda device: device.state.mist_virtual_level,
value_fn=lambda device: device.state.mist_level,
)
]

View File

@@ -49,17 +49,6 @@
}
}
},
"humidifier": {
"vesync": {
"state_attributes": {
"mode": {
"state": {
"humidity": "Humidity"
}
}
}
}
},
"number": {
"mist_level": {
"name": "Mist level"

View File

@@ -283,14 +283,6 @@
"default": "mdi:alarm-light"
}
},
"device_tracker": {
"location": {
"default": "mdi:car",
"state": {
"not_home": "mdi:car-arrow-right"
}
}
},
"sensor": {
"availability": {
"default": "mdi:car-connected"

View File

@@ -210,11 +210,6 @@
"name": "Honk & flash"
}
},
"device_tracker": {
"location": {
"name": "Location"
}
},
"sensor": {
"availability": {
"name": "Car connection",

View File

@@ -17,7 +17,6 @@ from homeassistant.components.homeassistant_hardware.helpers import (
async_notify_firmware_info,
async_register_firmware_info_provider,
)
from homeassistant.components.usb import usb_device_from_path
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_TYPE,
@@ -135,21 +134,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
Will automatically load components to support devices found on the network.
"""
# Try to perform an in-place migration if we detect that the device path can be made
# unique
device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
usb_device = await hass.async_add_executor_job(usb_device_from_path, device_path)
if usb_device is not None and device_path != usb_device.device:
_LOGGER.info(
"Migrating ZHA device path from %s to %s", device_path, usb_device.device
)
new_data = {**config_entry.data}
new_data[CONF_DEVICE][CONF_DEVICE_PATH] = usb_device.device
hass.config_entries.async_update_entry(config_entry, data=new_data)
device_path = usb_device.device
ha_zha_data: HAZHAData = get_zha_data(hass)
ha_zha_data.config_entry = config_entry
zha_lib_data: ZHAData = create_zha_config(hass, ha_zha_data)
@@ -179,6 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
_LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache)
# Check if firmware update is in progress for this device
device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
_raise_if_port_in_use(hass, device_path)
try:

View File

@@ -7,9 +7,10 @@ import collections
from contextlib import suppress
from enum import StrEnum
import json
import os
from typing import Any
import serial.tools.list_ports
from serial.tools.list_ports_common import ListPortInfo
import voluptuous as vol
from zha.application.const import RadioType
import zigpy.backups
@@ -24,7 +25,6 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
ZigbeeFlowStrategy,
)
from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware
from homeassistant.components.usb import USBDevice, scan_serial_ports
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_ZEROCONF,
@@ -124,10 +124,10 @@ def _format_backup_choice(
return f"{dt_util.as_local(backup.backup_time).strftime('%c')} ({identifier})"
async def list_serial_ports(hass: HomeAssistant) -> list[USBDevice]:
async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]:
"""List all serial ports, including the Yellow radio and the multi-PAN addon."""
ports: list[USBDevice] = []
ports.extend(await hass.async_add_executor_job(scan_serial_ports))
ports: list[ListPortInfo] = []
ports.extend(await hass.async_add_executor_job(serial.tools.list_ports.comports))
# Add useful info to the Yellow's serial port selection screen
try:
@@ -137,14 +137,9 @@ async def list_serial_ports(hass: HomeAssistant) -> list[USBDevice]:
else:
# PySerial does not properly handle the Yellow's serial port with the CM5
# so we manually include it
port = USBDevice(
device="/dev/ttyAMA1",
vid="ffff", # This is technically not a USB device
pid="ffff",
serial_number=None,
manufacturer="Nabu Casa",
description="Yellow Zigbee module",
)
port = ListPortInfo(device="/dev/ttyAMA1", skip_link_detection=True)
port.description = "Yellow Zigbee module"
port.manufacturer = "Nabu Casa"
ports = [p for p in ports if not p.device.startswith("/dev/ttyAMA")]
ports.insert(0, port)
@@ -161,15 +156,13 @@ async def list_serial_ports(hass: HomeAssistant) -> list[USBDevice]:
addon_info = None
if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED:
addon_port = USBDevice(
addon_port = ListPortInfo(
device=silabs_multiprotocol_addon.get_zigbee_socket(),
vid="ffff", # This is technically not a USB device
pid="ffff",
serial_number=None,
manufacturer="Nabu Casa",
description="Silicon Labs Multiprotocol add-on",
skip_link_detection=True,
)
addon_port.description = "Multiprotocol add-on"
addon_port.manufacturer = "Nabu Casa"
ports.append(addon_port)
return ports
@@ -225,15 +218,8 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
) -> ConfigFlowResult:
"""Choose a serial port."""
ports = await list_serial_ports(self.hass)
# The full `/dev/serial/by-id/` path is too verbose to show
resolved_paths = {
p.device: await self.hass.async_add_executor_job(os.path.realpath, p.device)
for p in ports
}
list_of_ports = [
f"{resolved_paths[p.device]} - {p.description}{', s/n: ' + p.serial_number if p.serial_number else ''}"
f"{p}{', s/n: ' + p.serial_number if p.serial_number else ''}"
+ (f" - {p.manufacturer}" if p.manufacturer else "")
for p in ports
]
@@ -487,63 +473,10 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
temp_radio_mgr.device_settings = config_entry.data[CONF_DEVICE]
temp_radio_mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]]
try:
await temp_radio_mgr.async_reset_adapter()
except HomeAssistantError:
# Old adapter not found or cannot connect, show prompt to plug back in
return await self.async_step_plug_in_old_radio()
await temp_radio_mgr.async_reset_adapter()
return await self.async_step_maybe_confirm_ezsp_restore()
async def async_step_plug_in_old_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Prompt user to plug in the old radio if connection fails."""
config_entries = self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False
)
# Unless the user removes the config entry whilst we try to reset the old radio
# for a few seconds and then also unplugs it, we will basically never hit this
if not config_entries:
return await self.async_step_maybe_confirm_ezsp_restore()
config_entry = config_entries[0]
old_device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
return self.async_show_menu(
step_id="plug_in_old_radio",
menu_options=["retry_old_radio", "skip_reset_old_radio"],
description_placeholders={"device_path": old_device_path},
)
async def async_step_retry_old_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Retry connecting to the old radio."""
return await self.async_step_maybe_reset_old_radio()
async def async_step_skip_reset_old_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Skip resetting the old radio and continue with migration."""
return await self.async_step_maybe_confirm_ezsp_restore()
async def async_step_plug_in_new_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Prompt user to plug in the new radio if connection fails."""
if user_input is not None:
# User confirmed, retry now
return await self.async_step_maybe_confirm_ezsp_restore()
assert self._radio_mgr.device_path is not None
return self.async_show_form(
step_id="plug_in_new_radio",
description_placeholders={"device_path": self._radio_mgr.device_path},
)
async def async_step_migration_strategy_advanced(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -700,9 +633,6 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
# On confirmation, overwrite destructively
try:
await self._radio_mgr.restore_backup(overwrite_ieee=True)
except HomeAssistantError:
# User unplugged the new adapter, allow retry
return await self.async_step_plug_in_new_radio()
except CannotWriteNetworkSettings as exc:
return self.async_abort(
reason="cannot_restore_backup",
@@ -720,9 +650,6 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
except DestructiveWriteNetworkSettings:
# Restore cannot happen automatically, we need to ask for permission
pass
except HomeAssistantError:
# User unplugged the new adapter, allow retry
return await self.async_step_plug_in_new_radio()
except CannotWriteNetworkSettings as exc:
return self.async_abort(
reason="cannot_restore_backup",

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.77"],
"requirements": ["zha==0.0.75"],
"usb": [
{
"description": "*2652*",

View File

@@ -111,22 +111,6 @@
"description": "A backup was created earlier and your old adapter is being reset as part of the migration.",
"title": "Resetting old adapter"
},
"plug_in_new_radio": {
"description": "Your new adapter at `{device_path}` was not found.\nPlease plug it in and click Submit to continue.",
"title": "New adapter not found"
},
"plug_in_old_radio": {
"description": "Your old adapter at `{device_path}` was not found. You can retry after plugging it back in, or skip resetting the old adapter.\n\n**Warning:** If you skip resetting the old adapter, ensure it remains permanently disconnected. Plugging it back in later will cause network issues.",
"menu_option_descriptions": {
"retry_old_radio": "Retry connecting to the old adapter to reset it as part of the migration.",
"skip_reset_old_radio": "Skip resetting the old adapter and continue with the migration."
},
"menu_options": {
"retry_old_radio": "Retry",
"skip_reset_old_radio": "Skip reset"
},
"title": "Old adapter not found"
},
"upload_manual_backup": {
"data": {
"uploaded_backup_file": "Upload a file"
@@ -1961,22 +1945,6 @@
"description": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::description%]",
"title": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::title%]"
},
"plug_in_new_radio": {
"description": "[%key:component::zha::config::step::plug_in_new_radio::description%]",
"title": "[%key:component::zha::config::step::plug_in_new_radio::title%]"
},
"plug_in_old_radio": {
"description": "[%key:component::zha::config::step::plug_in_old_radio::description%]",
"menu_option_descriptions": {
"retry_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_option_descriptions::retry_old_radio%]",
"skip_reset_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_option_descriptions::skip_reset_old_radio%]"
},
"menu_options": {
"retry_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_options::retry_old_radio%]",
"skip_reset_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_options::skip_reset_old_radio%]"
},
"title": "[%key:component::zha::config::step::plug_in_old_radio::title%]"
},
"prompt_migrate_or_reconfigure": {
"description": "Are you migrating to a new adapter or re-configuring the current adapter?",
"menu_option_descriptions": {

View File

@@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "0b4"
PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -23,12 +23,9 @@ SUPPORTED_REGIONS: Final[set[str]] = {
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-5",
"ca-central-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
@@ -43,7 +40,6 @@ SUPPORTED_REGIONS: Final[set[str]] = {
SUPPORTED_VOICES: Final[set[str]] = {
"Aditi",
"Adriano",
"Alba",
"Amy",
"Andres",
"Aria",
@@ -84,9 +80,6 @@ SUPPORTED_VOICES: Final[set[str]] = {
"Ivy",
"Jacek",
"Jan",
"Jasmine",
"Jihye",
"Jitka",
"Joanna",
"Joey",
"Justin",
@@ -118,17 +111,14 @@ SUPPORTED_VOICES: Final[set[str]] = {
"Nicole",
"Ola",
"Olivia",
"Patrick",
"Pedro",
"Penelope",
"Raul",
"Raveena",
"Remi",
"Ricardo",
"Ruben",
"Russell",
"Ruth",
"Sabrina",
"Salli",
"Seoyeon",
"Sergio",

View File

@@ -27,7 +27,6 @@ APPLICATION_CREDENTIALS = [
"miele",
"monzo",
"myuplink",
"neato",
"nest",
"netatmo",
"ondilo_ico",

View File

@@ -428,7 +428,6 @@ FLOWS = {
"nam",
"nanoleaf",
"nasweb",
"neato",
"nederlandse_spoorwegen",
"nest",
"netatmo",

View File

@@ -1767,6 +1767,11 @@
"config_flow": true,
"iot_class": "local_polling"
},
"enmax": {
"name": "Enmax Energy",
"integration_type": "virtual",
"supported_by": "opower"
},
"enocean": {
"name": "EnOcean",
"integration_type": "hub",
@@ -4334,12 +4339,6 @@
"integration_type": "virtual",
"supported_by": "opower"
},
"neato": {
"name": "Neato Botvac",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"nederlandse_spoorwegen": {
"name": "Nederlandse Spoorwegen (NS)",
"integration_type": "service",

View File

@@ -36,10 +36,10 @@ fnv-hash-fast==1.6.0
go2rtc-client==0.2.1
ha-ffmpeg==3.2.2
habluetooth==5.7.0
hass-nabucasa==1.5.1
hassil==3.4.0
hass-nabucasa==1.4.0
hassil==3.3.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20251104.0
home-assistant-frontend==20251001.4
home-assistant-intents==2025.10.28
httpx==0.28.1
ifaddr==0.2.0
@@ -69,7 +69,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==1.5.2
urllib3>=2.0
uv==0.9.6
uv==0.9.5
voluptuous-openapi==0.1.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2

View File

@@ -104,30 +104,20 @@ class Dialect:
return (-1, 0)
is_exact_language = self.language == dialect.language
is_exact_language_and_code = is_exact_language and (self.code == dialect.code)
if (self.region is None) and (dialect.region is None):
# Weak match with no region constraint
# Prefer exact language match
if is_exact_language_and_code:
return (3, 0)
if is_exact_language:
return (2, 0)
return (1, 0)
return (2 if is_exact_language else 1, 0)
if (self.region is not None) and (dialect.region is not None):
if self.region == dialect.region:
# Same language + region match
# Prefer exact language match
if is_exact_language_and_code:
return (math.inf, 2)
if is_exact_language:
return (math.inf, 1)
return (math.inf, 0)
return (
math.inf,
1 if is_exact_language else 0,
)
# Regions are both set, but don't match
return (0, 0)
@@ -149,8 +139,8 @@ class Dialect:
region_idx = pref_regions.index(dialect.region)
# More preferred regions are at the front.
# Add 2 to boost above a weak match where no regions are set.
return (2 + (len(pref_regions) - region_idx), 0)
# Add 1 to boost above a weak match where no regions are set.
return (1 + (len(pref_regions) - region_idx), 0)
except ValueError:
# Region was not in preferred list
pass

10
mypy.ini generated
View File

@@ -3366,16 +3366,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.neato.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.nest.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.11.0b4"
version = "2025.11.0.dev0"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -48,7 +48,7 @@ dependencies = [
"fnv-hash-fast==1.6.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
"hass-nabucasa==1.5.1",
"hass-nabucasa==1.4.0",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1",
@@ -75,7 +75,7 @@ dependencies = [
"typing-extensions>=4.15.0,<5.0",
"ulid-transform==1.5.2",
"urllib3>=2.0",
"uv==0.9.6",
"uv==0.9.5",
"voluptuous==0.15.2",
"voluptuous-serialize==2.7.0",
"voluptuous-openapi==0.1.0",

4
requirements.txt generated
View File

@@ -22,7 +22,7 @@ certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.6
fnv-hash-fast==1.6.0
hass-nabucasa==1.5.1
hass-nabucasa==1.4.0
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0
@@ -46,7 +46,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==1.5.2
urllib3>=2.0
uv==0.9.6
uv==0.9.5
voluptuous==0.15.2
voluptuous-serialize==2.7.0
voluptuous-openapi==0.1.0

43
requirements_all.txt generated
View File

@@ -194,7 +194,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.2
# homeassistant.components.alexa_devices
aioamazondevices==6.5.6
aioamazondevices==6.5.5
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -791,7 +791,7 @@ decora-wifi==1.4
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==16.3.0
deebot-client==16.1.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -859,7 +859,7 @@ ebusdpy==0.0.17
ecoaliface==0.4.0
# homeassistant.components.eheimdigital
eheimdigital==1.4.0
eheimdigital==1.3.0
# homeassistant.components.ekeybionyx
ekey-bionyxpy==1.0.0
@@ -1160,14 +1160,14 @@ habiticalib==0.4.6
habluetooth==5.7.0
# homeassistant.components.cloud
hass-nabucasa==1.5.1
hass-nabucasa==1.4.0
# homeassistant.components.splunk
hass-splunk==0.1.1
# homeassistant.components.assist_satellite
# homeassistant.components.conversation
hassil==3.4.0
hassil==3.3.0
# homeassistant.components.jewish_calendar
hdate[astral]==1.1.2
@@ -1201,7 +1201,7 @@ hole==0.9.0
holidays==0.83
# homeassistant.components.frontend
home-assistant-frontend==20251104.0
home-assistant-frontend==20251001.4
# homeassistant.components.conversation
home-assistant-intents==2025.10.28
@@ -1334,7 +1334,7 @@ kiwiki-client==0.1.1
knocki==0.4.2
# homeassistant.components.knx
knx-frontend==2025.10.31.195356
knx-frontend==2025.10.26.81530
# homeassistant.components.konnected
konnected==1.2.0
@@ -1376,7 +1376,7 @@ libpyfoscamcgi==0.0.8
libpyvivotek==0.6.1
# homeassistant.components.libre_hardware_monitor
librehardwaremonitor-api==1.5.0
librehardwaremonitor-api==1.4.0
# homeassistant.components.mikrotik
librouteros==3.2.0
@@ -1630,7 +1630,7 @@ omnilogic==0.4.5
ondilo==0.5.0
# homeassistant.components.onedrive
onedrive-personal-sdk==0.0.15
onedrive-personal-sdk==0.0.14
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1670,7 +1670,7 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
opower==0.15.9
opower==0.15.8
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1911,9 +1911,6 @@ pyblackbird==0.6
# homeassistant.components.bluesound
pyblu==2.0.5
# homeassistant.components.neato
pybotvac==0.0.28
# homeassistant.components.braviatv
pybravia==0.3.4
@@ -1951,7 +1948,7 @@ pycsspeechtts==1.0.8
# pycups==2.0.4
# homeassistant.components.cync
pycync==0.4.3
pycync==0.4.2
# homeassistant.components.daikin
pydaikin==2.17.1
@@ -2228,7 +2225,7 @@ pynetio==0.1.9.1
pynina==0.3.6
# homeassistant.components.nintendo_parental_controls
pynintendoparental==1.1.3
pynintendoparental==1.1.2
# homeassistant.components.nobo_hub
pynobo==1.8.1
@@ -2308,7 +2305,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.12
pyportainer==1.0.11
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2537,7 +2534,7 @@ python-mpd2==3.1.1
python-mystrom==2.5.0
# homeassistant.components.open_router
python-open-router==0.3.2
python-open-router==0.3.1
# homeassistant.components.swiss_public_transport
python-opendata-transport==0.5.0
@@ -2568,7 +2565,7 @@ python-ripple-api==0.0.3
python-roborock==3.7.0
# homeassistant.components.smarttub
python-smarttub==0.0.45
python-smarttub==0.0.44
# homeassistant.components.snoo
python-snoo==0.8.3
@@ -2635,7 +2632,7 @@ pyvera==0.3.16
pyversasense==0.0.6
# homeassistant.components.vesync
pyvesync==3.1.4
pyvesync==3.1.2
# homeassistant.components.vizio
pyvizio==0.1.61
@@ -2725,7 +2722,7 @@ renault-api==0.5.0
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.16.4
reolink-aio==0.16.2
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -2976,7 +2973,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==1.2.5
tesla-fleet-api==1.2.3
# homeassistant.components.powerwall
tesla-powerwall==0.5.2
@@ -3107,7 +3104,7 @@ vacuum-map-parser-roborock==0.1.4
vallox-websocket-api==5.3.0
# homeassistant.components.vegehub
vegehub==0.1.26
vegehub==0.1.24
# homeassistant.components.rdw
vehicle==2.2.2
@@ -3262,7 +3259,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.77
zha==0.0.75
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -182,7 +182,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.2
# homeassistant.components.alexa_devices
aioamazondevices==6.5.6
aioamazondevices==6.5.5
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -691,7 +691,7 @@ debugpy==1.8.16
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==16.3.0
deebot-client==16.1.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -750,7 +750,7 @@ eagle100==0.1.1
easyenergy==2.1.2
# homeassistant.components.eheimdigital
eheimdigital==1.4.0
eheimdigital==1.3.0
# homeassistant.components.ekeybionyx
ekey-bionyxpy==1.0.0
@@ -1021,11 +1021,11 @@ habiticalib==0.4.6
habluetooth==5.7.0
# homeassistant.components.cloud
hass-nabucasa==1.5.1
hass-nabucasa==1.4.0
# homeassistant.components.assist_satellite
# homeassistant.components.conversation
hassil==3.4.0
hassil==3.3.0
# homeassistant.components.jewish_calendar
hdate[astral]==1.1.2
@@ -1050,7 +1050,7 @@ hole==0.9.0
holidays==0.83
# homeassistant.components.frontend
home-assistant-frontend==20251104.0
home-assistant-frontend==20251001.4
# homeassistant.components.conversation
home-assistant-intents==2025.10.28
@@ -1159,7 +1159,7 @@ kegtron-ble==1.0.2
knocki==0.4.2
# homeassistant.components.knx
knx-frontend==2025.10.31.195356
knx-frontend==2025.10.26.81530
# homeassistant.components.konnected
konnected==1.2.0
@@ -1195,7 +1195,7 @@ letpot==0.6.3
libpyfoscamcgi==0.0.8
# homeassistant.components.libre_hardware_monitor
librehardwaremonitor-api==1.5.0
librehardwaremonitor-api==1.4.0
# homeassistant.components.mikrotik
librouteros==3.2.0
@@ -1401,7 +1401,7 @@ omnilogic==0.4.5
ondilo==0.5.0
# homeassistant.components.onedrive
onedrive-personal-sdk==0.0.15
onedrive-personal-sdk==0.0.14
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1429,7 +1429,7 @@ openrgb-python==0.3.6
openwebifpy==4.3.1
# homeassistant.components.opower
opower==0.15.9
opower==0.15.8
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1616,9 +1616,6 @@ pyblackbird==0.6
# homeassistant.components.bluesound
pyblu==2.0.5
# homeassistant.components.neato
pybotvac==0.0.28
# homeassistant.components.braviatv
pybravia==0.3.4
@@ -1644,7 +1641,7 @@ pycsspeechtts==1.0.8
# pycups==2.0.4
# homeassistant.components.cync
pycync==0.4.3
pycync==0.4.2
# homeassistant.components.daikin
pydaikin==2.17.1
@@ -1864,7 +1861,7 @@ pynetgear==0.10.10
pynina==0.3.6
# homeassistant.components.nintendo_parental_controls
pynintendoparental==1.1.3
pynintendoparental==1.1.2
# homeassistant.components.nobo_hub
pynobo==1.8.1
@@ -1935,7 +1932,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.12
pyportainer==1.0.11
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2110,7 +2107,7 @@ python-mpd2==3.1.1
python-mystrom==2.5.0
# homeassistant.components.open_router
python-open-router==0.3.2
python-open-router==0.3.1
# homeassistant.components.swiss_public_transport
python-opendata-transport==0.5.0
@@ -2138,7 +2135,7 @@ python-rabbitair==0.0.8
python-roborock==3.7.0
# homeassistant.components.smarttub
python-smarttub==0.0.45
python-smarttub==0.0.44
# homeassistant.components.snoo
python-snoo==0.8.3
@@ -2193,7 +2190,7 @@ pyuptimerobot==22.2.0
pyvera==0.3.16
# homeassistant.components.vesync
pyvesync==3.1.4
pyvesync==3.1.2
# homeassistant.components.vizio
pyvizio==0.1.61
@@ -2271,7 +2268,7 @@ renault-api==0.5.0
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.16.4
reolink-aio==0.16.2
# homeassistant.components.rflink
rflink==0.0.67
@@ -2465,7 +2462,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==1.2.5
tesla-fleet-api==1.2.3
# homeassistant.components.powerwall
tesla-powerwall==0.5.2
@@ -2578,7 +2575,7 @@ vacuum-map-parser-roborock==0.1.4
vallox-websocket-api==5.3.0
# homeassistant.components.vegehub
vegehub==0.1.26
vegehub==0.1.24
# homeassistant.components.rdw
vehicle==2.2.2
@@ -2709,7 +2706,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.77
zha==0.0.75
# homeassistant.components.zwave_js
zwave-js-server-python==0.67.1

View File

@@ -14,7 +14,7 @@ WORKDIR "/github/workspace"
COPY . /usr/src/homeassistant
# Uv is only needed during build
RUN --mount=from=ghcr.io/astral-sh/uv:0.9.6,source=/uv,target=/bin/uv \
RUN --mount=from=ghcr.io/astral-sh/uv:0.9.5,source=/uv,target=/bin/uv \
# Uv creates a lock file in /tmp
--mount=type=tmpfs,target=/tmp \
# Required for PyTurboJPEG
@@ -31,7 +31,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.9.6,source=/uv,target=/bin/uv \
PyTurboJPEG==1.8.0 \
go2rtc-client==0.2.1 \
ha-ffmpeg==3.2.2 \
hassil==3.4.0 \
hassil==3.3.0 \
home-assistant-intents==2025.10.28 \
mutagen==1.47.0 \
pymicro-vad==1.0.1 \

View File

@@ -673,7 +673,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"namecheapdns",
"nanoleaf",
"nasweb",
"neato",
"nederlandse_spoorwegen",
"ness_alarm",
"netatmo",
@@ -1706,7 +1705,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"namecheapdns",
"nanoleaf",
"nasweb",
"neato",
"nederlandse_spoorwegen",
"nest",
"ness_alarm",

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from functools import partial
import json
import re
import string
from typing import Any
import voluptuous as vol
@@ -132,12 +131,10 @@ def translation_value_validator(value: Any) -> str:
- prevents string with HTML
- prevents strings with single quoted placeholders
- prevents strings with placeholders using invalid identifiers
- prevents combined translations
"""
string_value = cv.string_with_no_html(value)
string_value = string_no_single_quoted_placeholders(string_value)
string_value = validate_placeholders(string_value)
if RE_COMBINED_REFERENCE.search(string_value):
raise vol.Invalid("the string should not contain combined translations")
if string_value != string_value.strip():
@@ -154,19 +151,6 @@ def string_no_single_quoted_placeholders(value: str) -> str:
return value
def validate_placeholders(value: str) -> str:
"""Validate that placeholders in translations use valid identifiers."""
formatter = string.Formatter()
for _, field_name, _, _ in formatter.parse(value):
if field_name: # skip literal text segments
if not field_name.isidentifier():
raise vol.Invalid(
"placeholders must be valid identifiers ([a-zA-Z_][a-zA-Z0-9_]*)"
)
return value
def gen_data_entry_schema(
*,
config: Config,

View File

@@ -3,11 +3,12 @@
import argparse
import json
from pathlib import Path
import re
import sys
from . import download, upload
from .const import INTEGRATIONS_DIR
from .util import flatten_translations, get_base_arg_parser, substitute_references
from .util import flatten_translations, get_base_arg_parser
def valid_integration(integration):
@@ -30,6 +31,42 @@ def get_arguments() -> argparse.Namespace:
return parser.parse_args()
def substitute_translation_references(integration_strings, flattened_translations):
"""Recursively processes all translation strings for the integration."""
result = {}
for key, value in integration_strings.items():
if isinstance(value, dict):
sub_dict = substitute_translation_references(value, flattened_translations)
result[key] = sub_dict
elif isinstance(value, str):
result[key] = substitute_reference(value, flattened_translations)
return result
def substitute_reference(value, flattened_translations):
"""Substitute localization key references in a translation string."""
matches = re.findall(r"\[\%key:([a-z0-9_]+(?:::(?:[a-z0-9-_])+)+)\%\]", value)
if not matches:
return value
new = value
for key in matches:
if key in flattened_translations:
new = new.replace(
f"[%key:{key}%]",
# New value can also be a substitution reference
substitute_reference(
flattened_translations[key], flattened_translations
),
)
else:
print(f"Invalid substitution key '{key}' found in string '{value}'")
sys.exit(1)
return new
def run_single(translations, flattened_translations, integration):
"""Run the script for a single integration."""
print(f"Generating translations for {integration}")
@@ -40,8 +77,8 @@ def run_single(translations, flattened_translations, integration):
integration_strings = translations["component"][integration]
translations["component"][integration] = substitute_references(
integration_strings, flattened_translations, fail_on_missing=True
translations["component"][integration] = substitute_translation_references(
integration_strings, flattened_translations
)
if download.DOWNLOAD_DIR.is_dir():

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