Merge branch 'dev' into epenet-20250527-1510

This commit is contained in:
epenet
2025-06-01 19:28:54 +02:00
committed by GitHub
430 changed files with 4578 additions and 2823 deletions

View File

@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

View File

@@ -0,0 +1,6 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}

View File

@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries()
}
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError):
async with timeout(5):
hubs: list[aiopulse.Hub] = [
hubs = [
hub
async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
)
from . import AgentDVRConfigEntry
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
)
self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Agent",
model="Camera",
name=f"{device.client.name} {device.name}",

View File

@@ -5,23 +5,22 @@ from __future__ import annotations
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from airthings import Airthings
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SECRET, DOMAIN
from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
@@ -32,21 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
async def _update_method() -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -0,0 +1,36 @@
"""The Airthings integration."""
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,
)
self.airthings = airthings
async def _update_method(self) -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await self.airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err

View File

@@ -19,6 +19,7 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -27,8 +28,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
from . import AirthingsConfigEntry
from .const import DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator
SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription(
@@ -54,6 +56,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
"sla": SensorEntityDescription(
key="sla",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
),
"battery": SensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
@@ -140,7 +148,7 @@ async def async_setup_entry(
class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
):
"""Representation of a Airthings Sensor device."""
@@ -149,7 +157,7 @@ class AirthingsHeaterEnergySensor(
def __init__(
self,
coordinator: AirthingsDataCoordinatorType,
coordinator: AirthingsDataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
) -> None:

View File

@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.positive_int,
vol.Required(CONF_CODE): cv.string,
}
),
)

View File

@@ -4,25 +4,114 @@
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "007147*" },
{ "macaddress": "00FC8B*" },
{ "macaddress": "0812A5*" },
{ "macaddress": "086AE5*" },
{ "macaddress": "08849D*" },
{ "macaddress": "089115*" },
{ "macaddress": "08A6BC*" },
{ "macaddress": "08C224*" },
{ "macaddress": "0CDC91*" },
{ "macaddress": "0CEE99*" },
{ "macaddress": "1009F9*" },
{ "macaddress": "109693*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "10CE02*" },
{ "macaddress": "140AC5*" },
{ "macaddress": "149138*" },
{ "macaddress": "1848BE*" },
{ "macaddress": "1C12B0*" },
{ "macaddress": "1C4D66*" },
{ "macaddress": "1C93C4*" },
{ "macaddress": "1CFE2B*" },
{ "macaddress": "244CE3*" },
{ "macaddress": "24CE33*" },
{ "macaddress": "2873F6*" },
{ "macaddress": "2C71FF*" },
{ "macaddress": "34AFB3*" },
{ "macaddress": "34D270*" },
{ "macaddress": "38F73D*" },
{ "macaddress": "3C5CC4*" },
{ "macaddress": "3CE441*" },
{ "macaddress": "440049*" },
{ "macaddress": "40A2DB*" },
{ "macaddress": "40A9CF*" },
{ "macaddress": "40B4CD*" },
{ "macaddress": "443D54*" },
{ "macaddress": "44650D*" },
{ "macaddress": "485F2D*" },
{ "macaddress": "48785E*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "4CEFC0*" },
{ "macaddress": "5007C3*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "50F5DA*" },
{ "macaddress": "5C415A*" },
{ "macaddress": "6837E9*" },
{ "macaddress": "6854FD*" },
{ "macaddress": "689A87*" },
{ "macaddress": "68B691*" },
{ "macaddress": "68DBF5*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "6C5697*" },
{ "macaddress": "7458F3*" },
{ "macaddress": "74C246*" },
{ "macaddress": "74D637*" },
{ "macaddress": "74E20C*" },
{ "macaddress": "74ECB2*" },
{ "macaddress": "786C84*" },
{ "macaddress": "78A03F*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "7C6305*" },
{ "macaddress": "7CD566*" },
{ "macaddress": "8871E5*" },
{ "macaddress": "901195*" },
{ "macaddress": "90235B*" },
{ "macaddress": "90A822*" },
{ "macaddress": "90F82E*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "98CCF3*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A002DC*" },
{ "macaddress": "A0D2B1*" },
{ "macaddress": "A40801*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "AC416A*" },
{ "macaddress": "AC63BE*" },
{ "macaddress": "ACCCFC*" },
{ "macaddress": "B0739C*" },
{ "macaddress": "B0CFCB*" },
{ "macaddress": "B0F7C4*" },
{ "macaddress": "B85F98*" },
{ "macaddress": "C091B9*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "C49500*" },
{ "macaddress": "C86C3D*" },
{ "macaddress": "CC9EA2*" },
{ "macaddress": "CCF735*" },
{ "macaddress": "DC54D7*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" }
{ "macaddress": "D8FBD6*" },
{ "macaddress": "DC91BF*" },
{ "macaddress": "DCA0D0*" },
{ "macaddress": "E0F728*" },
{ "macaddress": "EC2BEB*" },
{ "macaddress": "EC8AC4*" },
{ "macaddress": "ECA138*" },
{ "macaddress": "F02F9E*" },
{ "macaddress": "F0272D*" },
{ "macaddress": "F0F0A4*" },
{ "macaddress": "F4032A*" },
{ "macaddress": "F854B8*" },
{ "macaddress": "FC492D*" },
{ "macaddress": "FC65DE*" },
{ "macaddress": "FCA183*" },
{ "macaddress": "FCE9D8*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",

View File

@@ -5,7 +5,7 @@
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password sent to your email address."
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
},
"config": {
"flow_title": "{username}",

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.9.0"]
"requirements": ["pyaprilaire==0.9.1"]
}

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
from ..const import ATTR_MANUFACTURER, DOMAIN
from .config import AxisConfig
from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource
@@ -79,7 +79,7 @@ class AxisHub:
config_entry_id=self.config.entry.entry_id,
configuration_url=self.api.config.url,
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
identifiers={(AXIS_DOMAIN, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer=ATTR_MANUFACTURER,
model=f"{self.config.model} {self.product_type}",
name=self.config.name,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
}

View File

@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from .const import CONF_GESTURE, DOMAIN
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
from .device_trigger import (
CONF_BOTH_BUTTONS,
@@ -200,6 +200,6 @@ def async_describe_events(
}
async_describe_event(
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
)
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)

View File

@@ -1 +1,3 @@
"""The decora component."""
DOMAIN = "decora"

View File

@@ -21,7 +21,11 @@ from homeassistant.components.light import (
LightEntity,
)
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from . import DOMAIN
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
@@ -90,6 +94,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up an Decora switch."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Leviton Decora",
},
)
lights = []
for address, device_config in config[CONF_DEVICES].items():
device = {}

View File

@@ -1 +1,3 @@
"""The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"

View File

@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Detect",
},
)
source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))

View File

@@ -1 +1,4 @@
"""The dlib_face_identify component."""
CONF_FACES = "faces"
DOMAIN = "dlib_face_identify"

View File

@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{
@@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Identify",
},
)
confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE]

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import contextlib
from typing import Any
from typing import Any, Literal
import aiodns
from aiodns.error import DNSError
@@ -62,16 +62,16 @@ async def async_validate_hostname(
"""Validate hostname."""
async def async_check(
hostname: str, resolver: str, qtype: str, port: int = 53
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
) -> bool:
"""Return if able to resolve hostname."""
result = False
result: bool = False
with contextlib.suppress(DNSError):
result = bool(
await aiodns.DNSResolver( # type: ignore[call-overload]
_resolver = aiodns.DNSResolver(
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
)
result = bool(await _resolver.query(hostname, qtype))
return result
result: dict[str, 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==20250526.0"]
"requirements": ["home-assistant-frontend==20250531.0"]
}

View File

@@ -50,7 +50,12 @@ from .const import (
UNITS_IMPERIAL,
UNITS_METRIC,
)
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema(
{
@@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION],
)
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException:
return {"base": "invalid_auth"}
except TimeoutError:

View File

@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden,
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
from google.maps.routing_v2 import (
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try:
formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid):
except (AttributeError, vol.Invalid):
return Waypoint(address=location)
return Waypoint(
location=Location(
@@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)]
)
except PermissionDenied as permission_error:
_LOGGER.error("Permission denied: %s", permission_error.message)
raise PermissionDeniedException from permission_error
except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception):
"""Unknown API Error."""
class PermissionDeniedException(Exception):
"""Permission Denied Error."""
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Create an issue for the Routes API being disabled."""
async_create_issue(
hass,
DOMAIN,
f"routes_api_disabled_{entry.entry_id}",
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="routes_api_disabled",
translation_placeholders={
"entry_title": entry.title,
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
},
)
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Delete the issue for the Routes API being disabled."""
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")

View File

@@ -7,7 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import GoogleAPIError
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
from google.maps.routing_v2 import (
ComputeRoutesRequest,
Route,
@@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM,
)
from .helpers import convert_to_waypoint
from .helpers import (
convert_to_waypoint,
create_routes_api_disabled_issue,
delete_routes_api_disabled_issue,
)
_LOGGER = logging.getLogger(__name__)
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
)
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0:
self._route = response.routes[0]
delete_routes_api_disabled_issue(self.hass, self._config_entry)
except PermissionDenied:
_LOGGER.error("Routes API is disabled for this API key")
create_routes_api_disabled_issue(self.hass, self._config_entry)
self._route = None
except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex)
self._route = None

View File

@@ -21,6 +21,7 @@
}
},
"error": {
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers"
}
}
},
"issues": {
"routes_api_disabled": {
"title": "The Routes API must be enabled",
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
}
}
}

View File

@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
from .entity import GroupEntity
DEFAULT_NAME = "Sensor Group"
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return state_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_state_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return device_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_device_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
if device_class:
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_uoms_not_matching_device_class",
is_fixable=False,
is_persistent=False,
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
else:
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_uoms_not_matching_no_device_class",
is_fixable=False,
is_persistent=False,

View File

@@ -1 +1,3 @@
"""The gstreamer component."""
DOMAIN = "gstreamer"

View File

@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Gstreamer platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "GStreamer",
},
)
name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE)

View File

@@ -10,17 +10,17 @@
"macaddress": "C8D778*"
},
{
"hostname": "(bosch|siemens)-*",
"hostname": "(balay|bosch|neff|siemens)-*",
"macaddress": "68A40E*"
},
{
"hostname": "siemens-*",
"hostname": "(siemens|neff)-*",
"macaddress": "38B4D3*"
}
],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.0"],
"requirements": ["aiohomeconnect==0.17.1"],
"zeroconf": ["_homeconnect._tcp.local."]
}

View File

@@ -18,6 +18,10 @@
"title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"deprecated_system_packages_config_flow_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
},
"deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "silver",
"requirements": ["aioimmich==0.6.0"]
"requirements": ["aioimmich==0.7.0"]
}

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from logging import getLogger
import mimetypes
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.exceptions import ImmichError
@@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Immich media source."""
entries = hass.config_entries.async_entries(
DOMAIN, include_disabled=False, include_ignore=False
)
hass.http.register_view(ImmichMediaView(hass))
return ImmichMediaSource(hass, entries)
return ImmichMediaSource(hass)
class ImmichMediaSourceIdentifier:
@@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
def __init__(self, identifier: str) -> None:
"""Split identifier into parts."""
parts = identifier.split("/")
# coonfig_entry.unique_id/album_id/asset_it/filename
parts = identifier.split("|")
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
self.unique_id = parts[0]
self.album_id = parts[1] if len(parts) > 1 else None
self.asset_id = parts[2] if len(parts) > 2 else None
self.file_name = parts[3] if len(parts) > 2 else None
self.collection = parts[1] if len(parts) > 1 else None
self.collection_id = parts[2] if len(parts) > 2 else None
self.asset_id = parts[3] if len(parts) > 3 else None
self.file_name = parts[4] if len(parts) > 3 else None
self.mime_type = parts[5] if len(parts) > 3 else None
class ImmichMediaSource(MediaSource):
@@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
name = "Immich"
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize Immich media source."""
super().__init__(DOMAIN)
self.hass = hass
self.entries = entries
async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Immich is not configured")
return BrowseMediaSource(
domain=DOMAIN,
@@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_immich(item),
*await self._async_build_immich(item, entries),
],
)
async def _async_build_immich(
self, item: MediaSourceItem
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
if not item.identifier:
LOGGER.debug("Render all Immich instances")
return [
BrowseMediaSource(
domain=DOMAIN,
@@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
can_play=False,
can_expand=True,
)
for entry in self.entries
for entry in entries
]
identifier = ImmichMediaSourceIdentifier(item.identifier)
entry: ImmichConfigEntry | None = (
@@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
assert entry
immich_api = entry.runtime_data.api
if identifier.album_id is None:
# Get Albums
if identifier.collection is None:
LOGGER.debug("Render all collections for %s", entry.title)
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|albums",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="albums",
can_play=False,
can_expand=True,
)
]
if identifier.collection_id is None:
LOGGER.debug("Render all albums for %s", entry.title)
try:
albums = await immich_api.albums.async_get_all_albums()
except ImmichError:
@@ -118,21 +130,25 @@ class ImmichMediaSource(MediaSource):
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{item.identifier}/{album.album_id}",
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title=album.name,
can_play=False,
can_expand=True,
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg",
)
for album in albums
]
# Request items of album
LOGGER.debug(
"Render all assets of album %s for %s",
identifier.collection_id,
entry.title,
)
try:
album_info = await immich_api.albums.async_get_album_info(
identifier.album_id
identifier.collection_id
)
except ImmichError:
return []
@@ -141,17 +157,18 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}|"
f"{asset.asset_id}|"
f"{asset.file_name}|"
f"{asset.mime_type}"
),
media_class=MediaClass.IMAGE,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=False,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}",
)
for asset in album_info.assets
if asset.mime_type.startswith("image/")
@@ -161,17 +178,18 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}|"
f"{asset.asset_id}|"
f"{asset.file_name}|"
f"{asset.mime_type}"
),
media_class=MediaClass.VIDEO,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=True,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg",
)
for asset in album_info.assets
if asset.mime_type.startswith("video/")
@@ -181,17 +199,23 @@ class ImmichMediaSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
try:
identifier = ImmichMediaSourceIdentifier(item.identifier)
if identifier.file_name is None:
raise Unresolvable("No file name")
mime_type, _ = mimetypes.guess_type(identifier.file_name)
if not isinstance(mime_type, str):
raise Unresolvable("No file extension")
except IndexError as err:
raise Unresolvable(
f"Could not parse identifier: {item.identifier}"
) from err
if identifier.mime_type is None:
raise Unresolvable(
f"Could not resolve identifier that has no mime-type: {item.identifier}"
)
return PlayMedia(
(
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
),
mime_type,
identifier.mime_type,
)
@@ -212,10 +236,10 @@ class ImmichMediaView(HomeAssistantView):
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
raise HTTPNotFound
asset_id, file_name, size = location.split("/")
mime_type, _ = mimetypes.guess_type(file_name)
if not isinstance(mime_type, str):
raise HTTPNotFound
try:
asset_id, size, mime_type_base, mime_type_format = location.split("/")
except ValueError as err:
raise HTTPNotFound from err
entry: ImmichConfigEntry | None = (
self.hass.config_entries.async_entry_for_domain_unique_id(
@@ -226,7 +250,7 @@ class ImmichMediaView(HomeAssistantView):
immich_api = entry.runtime_data.api
# stream response for videos
if mime_type.startswith("video/"):
if mime_type_base == "video":
try:
resp = await immich_api.assets.async_play_video_stream(asset_id)
except ImmichError as exc:
@@ -243,4 +267,4 @@ class ImmichMediaView(HomeAssistantView):
image = await immich_api.assets.async_view_asset(asset_id, size)
except ImmichError as exc:
raise HTTPNotFound from exc
return Response(body=image, content_type=mime_type)
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyiskra"],
"requirements": ["pyiskra==0.1.15"]
"requirements": ["pyiskra==0.1.19"]
}

View File

@@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS
from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
@@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
coordinator = JellyfinDataUpdateCoordinator(
hass, entry, client, server_info, user_id
)
await coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)},
manufacturer=DEFAULT_NAME,
name=coordinator.server_name,
sw_version=coordinator.server_version,
)
entry.runtime_data = coordinator
entry.async_on_unload(client.stop)

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_NAME, DOMAIN
from .const import DOMAIN
from .coordinator import JellyfinDataUpdateCoordinator
@@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
"""Initialize the Jellyfin entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)},
manufacturer=DEFAULT_NAME,
name=coordinator.server_name,
sw_version=coordinator.server_version,
)

View File

@@ -11,8 +11,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard"
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Keyboard",
},
)
keyboard = PyKeyboard()
keyboard.special_key_assignment()

View File

@@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=15)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
_LOGGER = logging.getLogger(__name__)

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.6"]
"requirements": ["pylamarzocco==2.0.8"]
}

View File

@@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_on",
translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off",
translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,

View File

@@ -3,6 +3,7 @@
"name": "LG ThinQ",
"codeowners": ["@LG-ThinQ-Integration"],
"config_flow": true,
"dhcp": [{ "macaddress": "34E6E6*" }],
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.2.8"],
"requirements": ["python-linkplay==0.2.9"],
"zeroconf": ["_linkplay._tcp.local."]
}

View File

@@ -7,8 +7,9 @@ import time
import lirc
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIRC capability."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "LIRC",
},
)
# blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully
# on exit.

View File

@@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MinPINCodeLength",
translation_key="min_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MaxPINCodeLength",
translation_key="max_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="TargetPositionLiftPercent100ths",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
translation_key="window_covering_target_position",
measurement_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE,

View File

@@ -390,12 +390,6 @@
"evse_user_max_charge_current": {
"name": "User max charge current"
},
"min_pin_code_length": {
"name": "Min PIN code length"
},
"max_pin_code_length": {
"name": "Max PIN code length"
},
"window_covering_target_position": {
"name": "Target opening position"
}

View File

@@ -63,16 +63,6 @@
}
}
},
"issues": {
"deprecated_yaml_import_issue_invalid_auth": {
"title": "The MELCloud YAML configuration import failed",
"description": "Configuring MELCloud using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
},
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The MELCloud YAML configuration import failed",
"description": "Configuring MELCloud using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to MELCloud works and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
}
},
"entity": {
"sensor": {
"room_temperature": {

View File

@@ -39,6 +39,7 @@ from homeassistant.components.light import (
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
STATE_CLASS_UNITS,
SensorDeviceClass,
SensorStateClass,
)
@@ -640,6 +641,13 @@ def validate_sensor_platform_config(
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
if (
(state_class := config.get(CONF_STATE_CLASS)) is not None
and state_class in STATE_CLASS_UNITS
and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class]
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class"
return errors
@@ -676,11 +684,19 @@ class PlatformField:
@callback
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
"""Return a context based unit of measurement selector."""
if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS:
return SelectSelector(
SelectSelectorConfig(
options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]],
sort=True,
custom_value=True,
)
)
if (
user_data is None
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
or device_class not in DEVICE_CLASS_UNITS
):
device_class := user_data.get(CONF_DEVICE_CLASS)
) is None or device_class not in DEVICE_CLASS_UNITS:
return TEXT_SELECTOR
return SelectSelector(
SelectSelectorConfig(

View File

@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT,
STATE_CLASS_UNITS,
STATE_CLASSES_SCHEMA,
RestoreSensor,
SensorDeviceClass,
@@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
)
if (
(state_class := config.get(CONF_STATE_CLASS)) is not None
and state_class in STATE_CLASS_UNITS
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT))
not in STATE_CLASS_UNITS[state_class]
):
raise vol.Invalid(
f"The unit of measurement '{unit_of_measurement}' is not valid "
f"together with state class '{state_class}'"
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:

View File

@@ -644,6 +644,7 @@
"invalid_template": "Invalid template",
"invalid_supported_color_modes": "Invalid supported color modes selection",
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_url": "Invalid URL",
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",

View File

@@ -58,15 +58,3 @@ class NikoHomeControlConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
"""Import a config entry."""
self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]})
error = await test_connection(import_info[CONF_HOST])
if not error:
return self.async_create_entry(
title="Niko Home Control",
data={CONF_HOST: import_info[CONF_HOST]},
)
return self.async_abort(reason=error)

View File

@@ -5,80 +5,19 @@ from __future__ import annotations
from typing import Any
from nhc.light import NHCLight
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
brightness_supported,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NHCController, NikoHomeControlConfigEntry
from .const import DOMAIN
from .entity import NikoHomeControlEntity
# delete after 2025.7.0
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Niko Home Control light platform."""
# Start import flow
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
if (
result.get("type") == FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result['reason']}",
breaks_in_ha_version="2025.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Niko Home Control",
},
)
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Niko Home Control",
},
)
async def async_setup_entry(
hass: HomeAssistant,

View File

@@ -17,11 +17,5 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"title": "YAML import failed due to a connection error",
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
}
}
}

View File

@@ -40,7 +40,7 @@ SUPPORT_FLAGS = (
PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
MIN_TEMPERATURE = 7
MAX_TEMPERATURE = 40
MAX_TEMPERATURE = 30
async def async_setup_entry(

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NukiEntryData
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN as NUKI_DOMAIN, ERROR_STATES
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN, ERROR_STATES
from .entity import NukiEntity
from .helpers import CannotConnect
@@ -29,7 +29,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Nuki lock platform."""
entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id]
entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id]
coordinator = entry_data.coordinator
entities: list[NukiDeviceEntity] = [

View File

@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
from .entity import OneWireEntity, OneWireEntityDescription
from .onewirehub import (
SIGNAL_NEW_DEVICE_CONNECTED,
@@ -37,13 +37,14 @@ class OneWireBinarySensorEntityDescription(
):
"""Class describing OneWire binary sensor entities."""
read_mode = READ_MODE_INT
DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
"12": tuple(
OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id",
translation_placeholders={"id": str(device_key)},
)
@@ -53,7 +54,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id",
translation_placeholders={"id": str(device_key)},
)
@@ -63,7 +63,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id",
translation_placeholders={"id": str(device_key)},
)
@@ -78,7 +77,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
OneWireBinarySensorEntityDescription(
key=f"hub/short.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.PROBLEM,
translation_key="hub_short_id",
@@ -162,4 +160,4 @@ class OneWireBinarySensorEntity(OneWireEntity, BinarySensorEntity):
"""Return true if sensor is on."""
if self._state is None:
return None
return bool(self._state)
return self._state == 1

View File

@@ -51,6 +51,5 @@ MANUFACTURER_MAXIM = "Maxim Integrated"
MANUFACTURER_HOBBYBOARDS = "Hobby Boards"
MANUFACTURER_EDS = "Embedded Data Systems"
READ_MODE_BOOL = "bool"
READ_MODE_FLOAT = "float"
READ_MODE_INT = "int"

View File

@@ -10,9 +10,8 @@ from pyownet import protocol
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import StateType
from .const import READ_MODE_BOOL, READ_MODE_INT
from .const import READ_MODE_INT
@dataclass(frozen=True)
@@ -45,7 +44,7 @@ class OneWireEntity(Entity):
self._attr_unique_id = f"/{device_id}/{description.key}"
self._attr_device_info = device_info
self._device_file = device_file
self._state: StateType = None
self._state: int | float | None = None
self._value_raw: float | None = None
self._owproxy = owproxy
@@ -82,7 +81,5 @@ class OneWireEntity(Entity):
_LOGGER.debug("Fetching %s data recovered", self.name)
if self.entity_description.read_mode == READ_MODE_INT:
self._state = int(self._value_raw)
elif self.entity_description.read_mode == READ_MODE_BOOL:
self._state = int(self._value_raw) == 1
else:
self._state = self._value_raw

View File

@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
from .entity import OneWireEntity, OneWireEntityDescription
from .onewirehub import (
SIGNAL_NEW_DEVICE_CONNECTED,
@@ -32,13 +32,14 @@ SCAN_INTERVAL = timedelta(seconds=30)
class OneWireSwitchEntityDescription(OneWireEntityDescription, SwitchEntityDescription):
"""Class describing OneWire switch entities."""
read_mode = READ_MODE_INT
DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
"05": (
OneWireSwitchEntityDescription(
key="PIO",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio",
),
),
@@ -47,7 +48,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"PIO.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id",
translation_placeholders={"id": str(device_key)},
)
@@ -57,7 +57,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"latch.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="latch_id",
translation_placeholders={"id": str(device_key)},
)
@@ -69,7 +68,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key="IAD",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
read_mode=READ_MODE_BOOL,
translation_key="iad",
),
),
@@ -78,7 +76,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"PIO.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id",
translation_placeholders={"id": str(device_key)},
)
@@ -88,7 +85,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"latch.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="latch_id",
translation_placeholders={"id": str(device_key)},
)
@@ -99,7 +95,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"PIO.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id",
translation_placeholders={"id": str(device_key)},
)
@@ -115,7 +110,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"hub/branch.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
translation_key="hub_branch_id",
translation_placeholders={"id": str(device_key)},
@@ -127,7 +121,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"moisture/is_leaf.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
translation_key="leaf_sensor_id",
translation_placeholders={"id": str(device_key)},
@@ -138,7 +131,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription(
key=f"moisture/is_moisture.{device_key}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
translation_key="moisture_sensor_id",
translation_placeholders={"id": str(device_key)},
@@ -226,7 +218,7 @@ class OneWireSwitchEntity(OneWireEntity, SwitchEntity):
"""Return true if switch is on."""
if self._state is None:
return None
return bool(self._state)
return self._state == 1
def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.12.2"]
"requirements": ["opower==0.12.3"]
}

View File

@@ -1 +1,3 @@
"""The pandora component."""
DOMAIN = "pandora"

View File

@@ -27,10 +27,13 @@ from homeassistant.const import (
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_UP,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -53,6 +56,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Pandora media player platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Pandora",
},
)
if not _pianobar_exists():
return
pandora = PandoraMediaPlayer("Pandora")

View File

@@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["pyprobeplus==1.0.0"]
"requirements": ["pyprobeplus==1.0.1"]
}

View File

@@ -7,7 +7,7 @@
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": [
"SQLAlchemy==2.0.40",
"SQLAlchemy==2.0.41",
"fnv-hash-fast==1.5.0",
"psutil-home-assistant==0.0.1"
]

View File

@@ -233,6 +233,14 @@ async def async_setup_entry(
"privacy_mode_change", async_privacy_mode_change, 623
)
# ensure host device is setup before connected camera devices that use via_device
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, host.unique_id)},
connections={(dr.CONNECTION_NETWORK_MAC, host.api.mac_address)},
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(

View File

@@ -462,6 +462,12 @@
"doorbell_button_sound": {
"default": "mdi:volume-high"
},
"hardwired_chime_enabled": {
"default": "mdi:bell",
"state": {
"off": "mdi:bell-off"
}
},
"hdr": {
"default": "mdi:hdr"
},

View File

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

View File

@@ -910,6 +910,9 @@
"auto_focus": {
"name": "Auto focus"
},
"hardwired_chime_enabled": {
"name": "Hardwired chime enabled"
},
"guard_return": {
"name": "Guard return"
},

View File

@@ -216,6 +216,16 @@ SWITCH_ENTITIES = (
value=lambda api, ch: api.baichuan.privacy_mode(ch),
method=lambda api, ch, value: api.baichuan.set_privacy_mode(ch, value),
),
ReolinkSwitchEntityDescription(
key="hardwired_chime_enabled",
cmd_key="483",
translation_key="hardwired_chime_enabled",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
supported=lambda api, ch: api.supported(ch, "hardwired_chime"),
value=lambda api, ch: api.baichuan.hardwired_chime_enabled(ch),
method=lambda api, ch, value: api.baichuan.set_ding_dong_ctrl(ch, enable=value),
),
)
NVR_SWITCH_ENTITIES = (

View File

@@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView):
verify_ssl=False,
ssl_cipher=SSLCipherList.INSECURE,
)
self._vod_type: str | None = None
async def get(
self,
@@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView):
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
ch = int(channel)
if self._vod_type is not None:
vod_type = self._vod_type
try:
host = get_host(self.hass, config_entry_id)
except Unresolvable:
@@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView):
"apolication/octet-stream",
]:
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
if (
reolink_response.content_type == "video/x-flv"
and vod_type == VodRequestType.PLAYBACK.value
):
# next time use DOWNLOAD immediately
self._vod_type = VodRequestType.DOWNLOAD.value
_LOGGER.debug(
"%s, retrying using download instead of playback cmd", err_str
)
return await self.get(
request,
config_entry_id,
channel,
stream_res,
self._vod_type,
filename,
retry,
)
_LOGGER.error(err_str)
if reolink_response.content_type == "text/html":
text = await reolink_response.text()
@@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView):
reolink_response.reason,
response_headers,
)
response_headers["Content-Type"] = "video/mp4"
if "Content-Type" not in response_headers:
response_headers["Content-Type"] = reolink_response.content_type
if response_headers["Content-Type"] == "apolication/octet-stream":
response_headers["Content-Type"] = "application/octet-stream"
response = web.StreamResponse(
status=reolink_response.status,

View File

@@ -92,13 +92,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"""Mark the first item with matching `name` as completed."""
data = hass.data[DOMAIN]
name = call.data[ATTR_NAME]
try:
item = [item for item in data.items if item["name"] == name][0]
except IndexError:
_LOGGER.error("Updating of item failed: %s cannot be found", name)
else:
await data.async_update(item["id"], {"name": name, "complete": True})
await data.async_complete(name)
except NoMatchingShoppingListItem:
_LOGGER.error("Completing of item failed: %s cannot be found", name)
async def incomplete_item_service(call: ServiceCall) -> None:
"""Mark the first item with matching `name` as incomplete."""
@@ -258,6 +255,30 @@ class ShoppingData:
)
return removed
async def async_complete(
self, name: str, context: Context | None = None
) -> list[dict[str, JsonValueType]]:
"""Mark all shopping list items with the given name as complete."""
complete_items = [
item for item in self.items if item["name"] == name and not item["complete"]
]
if len(complete_items) == 0:
raise NoMatchingShoppingListItem
for item in complete_items:
_LOGGER.debug("Completing %s", item)
item["complete"] = True
await self.hass.async_add_executor_job(self.save)
self._async_notify()
for item in complete_items:
self.hass.bus.async_fire(
EVENT_SHOPPING_LIST_UPDATED,
{"action": "complete", "item": item},
context=context,
)
return complete_items
async def async_update(
self, item_id: str | None, info: dict[str, Any], context: Context | None = None
) -> dict[str, JsonValueType]:

View File

@@ -5,15 +5,17 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, intent
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED, NoMatchingShoppingListItem
INTENT_ADD_ITEM = "HassShoppingListAddItem"
INTENT_COMPLETE_ITEM = "HassShoppingListCompleteItem"
INTENT_LAST_ITEMS = "HassShoppingListLastItems"
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the Shopping List intents."""
intent.async_register(hass, AddItemIntent())
intent.async_register(hass, CompleteItemIntent())
intent.async_register(hass, ListTopItemsIntent())
@@ -36,6 +38,33 @@ class AddItemIntent(intent.IntentHandler):
return response
class CompleteItemIntent(intent.IntentHandler):
"""Handle CompleteItem intents."""
intent_type = INTENT_COMPLETE_ITEM
description = "Marks an item as completed on the shopping list"
slot_schema = {"item": cv.string}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"].strip()
try:
complete_items = await intent_obj.hass.data[DOMAIN].async_complete(item)
except NoMatchingShoppingListItem:
complete_items = []
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
response = intent_obj.create_response()
response.async_set_speech_slots({"completed_items": complete_items})
response.response_type = intent.IntentResponseType.ACTION_DONE
return response
class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents."""
@@ -47,7 +76,7 @@ class ListTopItemsIntent(intent.IntentHandler):
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response()
response: intent.IntentResponse = intent_obj.create_response()
if not items:
response.async_set_speech("There are no items on your shopping list")

View File

@@ -1,25 +1,37 @@
"""Common base for entities."""
from dataclasses import dataclass
from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity import Entity, EntityDescription
from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
@dataclass(frozen=True, kw_only=True)
class SmarlaEntityDescription(EntityDescription):
"""Class describing Swing2Sleep Smarla entities."""
service: str
property: str
class SmarlaBaseEntity(Entity):
"""Common Base Entity class for defining Smarla device."""
entity_description: SmarlaEntityDescription
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, federwiege: Federwiege, prop: Property) -> None:
def __init__(self, federwiege: Federwiege, desc: SmarlaEntityDescription) -> None:
"""Initialise the entity."""
self._property = prop
self.entity_description = desc
self._property = federwiege.get_property(desc.service, desc.property)
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, federwiege.serial_number)},
name=DEVICE_MODEL_NAME,

View File

@@ -3,7 +3,6 @@
from dataclasses import dataclass
from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
@@ -11,16 +10,13 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FederwiegeConfigEntry
from .entity import SmarlaBaseEntity
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
@dataclass(frozen=True, kw_only=True)
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
class SmarlaSwitchEntityDescription(SmarlaEntityDescription, SwitchEntityDescription):
"""Class describing Swing2Sleep Smarla switch entity."""
service: str
property: str
SWITCHES: list[SmarlaSwitchEntityDescription] = [
SmarlaSwitchEntityDescription(
@@ -55,17 +51,6 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
_property: Property[bool]
def __init__(
self,
federwiege: Federwiege,
desc: SmarlaSwitchEntityDescription,
) -> None:
"""Initialize a Smarla switch."""
prop = federwiege.get_property(desc.service, desc.property)
super().__init__(federwiege, prop)
self.entity_description = desc
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
@property
def is_on(self) -> bool:
"""Return the entity value to represent the entity state."""

View File

@@ -12,7 +12,7 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pysmlight==0.2.4"],
"requirements": ["pysmlight==0.2.5"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."

View File

@@ -6,9 +6,14 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
},
extra=vol.ALLOW_EXTRA,
)
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -52,6 +58,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure Gammu state machine."""
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
DEPRECATED_ISSUE_ID,
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_config_flow_integration",
translation_placeholders={
"integration_title": "SMS notifications via GSM-modem",
},
)
device = entry.data[CONF_DEVICE]
connection_mode = "at"
@@ -101,4 +120,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
await gateway.terminate_async()
if not hass.config_entries.async_loaded_entries(DOMAIN):
async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID)
return unload_ok

View File

@@ -7,8 +7,13 @@ import logging
import voluptuous as vol
from homeassistant.components import mqtt
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
ServiceCall,
)
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
DOMAIN = "snips"
@@ -91,6 +96,20 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate Snips component."""
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Snips",
},
)
# Make sure MQTT integration is enabled and the client is available
if not await mqtt.async_wait_for_mqtt_client(hass):

View File

@@ -6,5 +6,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling",
"requirements": ["SQLAlchemy==2.0.40", "sqlparse==0.5.0"]
"requirements": ["SQLAlchemy==2.0.41", "sqlparse==0.5.0"]
}

View File

@@ -26,14 +26,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN as SWITCH_DOMAIN
from .const import DOMAIN
DEFAULT_NAME = "Light Switch"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
}
)
@@ -76,7 +76,7 @@ class LightSwitch(LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Forward the turn_on command to the switch in this light switch."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
@@ -86,7 +86,7 @@ class LightSwitch(LightEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Forward the turn_off command to the switch in this light switch."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,

View File

@@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, ToggleEntity
from homeassistant.helpers.event import async_track_state_change_event
from .const import DOMAIN as SWITCH_AS_X_DOMAIN
from .const import DOMAIN
class BaseEntity(Entity):
@@ -61,7 +61,7 @@ class BaseEntity(Entity):
self._switch_entity_id = switch_entity_id
self._is_new_entity = (
registry.async_get_entity_id(domain, SWITCH_AS_X_DOMAIN, unique_id) is None
registry.async_get_entity_id(domain, DOMAIN, unique_id) is None
)
@callback
@@ -102,7 +102,7 @@ class BaseEntity(Entity):
if registry.async_get(self.entity_id) is not None:
registry.async_update_entity_options(
self.entity_id,
SWITCH_AS_X_DOMAIN,
DOMAIN,
self.async_generate_entity_options(),
)

View File

@@ -7,7 +7,13 @@ from dataclasses import dataclass, field
from logging import getLogger
from aiohttp import web
from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI
from switchbot_api import (
Device,
Remote,
SwitchBotAPI,
SwitchBotAuthenticationError,
SwitchBotConnectionError,
)
from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry
@@ -175,12 +181,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api = SwitchBotAPI(token=token, secret=secret)
try:
devices = await api.list_devices()
except InvalidAuth as ex:
except SwitchBotAuthenticationError as ex:
_LOGGER.error(
"Invalid authentication while connecting to SwitchBot API: %s", ex
)
return False
except CannotConnect as ex:
except SwitchBotConnectionError as ex:
raise ConfigEntryNotReady from ex
_LOGGER.debug("Devices: %s", devices)
coordinators_by_id: dict[str, SwitchBotCoordinator] = {}

View File

@@ -3,7 +3,11 @@
from logging import getLogger
from typing import Any
from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI
from switchbot_api import (
SwitchBotAPI,
SwitchBotAuthenticationError,
SwitchBotConnectionError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -36,9 +40,9 @@ class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN):
await SwitchBotAPI(
token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY]
).list_devices()
except CannotConnect:
except SwitchBotConnectionError:
errors["base"] = "cannot_connect"
except InvalidAuth:
except SwitchBotAuthenticationError:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")

View File

@@ -4,7 +4,7 @@ from asyncio import timeout
from logging import getLogger
from typing import Any
from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI
from switchbot_api import Device, Remote, SwitchBotAPI, SwitchBotConnectionError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -70,5 +70,5 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]):
status: Status = await self._api.get_status(self._device_id)
_LOGGER.debug("Refreshing %s with %s", self._device_id, status)
return status
except CannotConnect as err:
except SwitchBotConnectionError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["switchbot_api"],
"requirements": ["switchbot-api==2.3.1"]
"requirements": ["switchbot-api==2.4.0"]
}

View File

@@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
SCAN_INTERVAL = timedelta(minutes=5)
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5)
class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["aiotedee"],
"quality_scale": "platinum",
"requirements": ["aiotedee==0.2.20"]
"requirements": ["aiotedee==0.2.23"]
}

View File

@@ -1 +1,4 @@
"""The tensorflow component."""
DOMAIN = "tensorflow"
CONF_GRAPH = "graph"

View File

@@ -26,15 +26,21 @@ from homeassistant.const import (
CONF_SOURCE,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.pil import draw_box
from . import CONF_GRAPH, DOMAIN
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
DOMAIN = "tensorflow"
_LOGGER = logging.getLogger(__name__)
ATTR_MATCHES = "matches"
@@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom"
CONF_CATEGORIES = "categories"
CONF_CATEGORY = "category"
CONF_FILE_OUT = "file_out"
CONF_GRAPH = "graph"
CONF_LABELS = "labels"
CONF_LABEL_OFFSET = "label_offset"
CONF_LEFT = "left"
@@ -110,6 +115,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the TensorFlow image processing platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Tensorflow",
},
)
model_config = config[CONF_MODEL]
model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow")
labels = model_config.get(CONF_LABELS) or hass.config.path(

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.0.17"]
"requirements": ["tesla-fleet-api==1.1.1"]
}

View File

@@ -125,6 +125,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
key="charge_state_conn_charge_cable",
polling=True,
polling_value_fn=lambda x: x != "<invalid>",
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType(
lambda value: callback(value != "Unknown")
),
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),

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.0.17", "teslemetry-stream==0.7.9"]
"requirements": ["tesla-fleet-api==1.1.1", "teslemetry-stream==0.7.9"]
}

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.0.17"]
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.1.1"]
}

View File

@@ -20,6 +20,10 @@ STATES = {
"Stopped": MediaPlayerState.IDLE,
}
# Tesla uses 31 steps, in 0.333 increments up to 10.333
VOLUME_STEP = 1 / 31
VOLUME_FACTOR = 31 / 3 # 10.333
PARALLEL_UPDATES = 0
@@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
"""Vehicle Location Media Class."""
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_volume_step = VOLUME_STEP
def __init__(
self,
@@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
@property
def volume_level(self) -> float:
"""Volume level of the media player (0..1)."""
return self.get("vehicle_state_media_info_audio_volume", 0) / self.get(
"vehicle_state_media_info_audio_volume_max", 10.333333
)
return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR
@property
def media_duration(self) -> int | None:

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.device_registry import (
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN, PLATFORMS
from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN, PLATFORMS
from .config import UnifiConfig
from .entity_helper import UnifiEntityHelper
from .entity_loader import UnifiEntityLoader
@@ -104,7 +104,7 @@ class UnifiHub:
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(UNIFI_DOMAIN, self.config.entry.unique_id)},
identifiers={(DOMAIN, self.config.entry.unique_id)},
manufacturer=ATTR_MANUFACTURER,
model="UniFi Network Application",
name="UniFi Network",

View File

@@ -52,7 +52,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import UnifiConfigEntry
from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
from .const import ATTR_MANUFACTURER, DOMAIN
from .entity import (
HandlerT,
SubscriptionT,
@@ -367,14 +367,12 @@ def async_update_unique_id(hass: HomeAssistant, config_entry: UnifiConfigEntry)
def update_unique_id(obj_id: str, type_name: str) -> None:
"""Rework unique ID."""
new_unique_id = f"{type_name}-{obj_id}"
if ent_reg.async_get_entity_id(SWITCH_DOMAIN, UNIFI_DOMAIN, new_unique_id):
if ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, new_unique_id):
return
prefix, _, suffix = obj_id.partition("_")
unique_id = f"{prefix}-{type_name}-{suffix}"
if entity_id := ent_reg.async_get_entity_id(
SWITCH_DOMAIN, UNIFI_DOMAIN, unique_id
):
if entity_id := ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id):
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
for obj_id in hub.api.outlets:

View File

@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -300,7 +300,9 @@ async def handle_call_service(
translation_placeholders=err.translation_placeholders,
)
except HomeAssistantError as err:
connection.logger.exception("Unexpected exception")
connection.logger.error(
"Error during service call to %s.%s: %s", msg["domain"], msg["service"], err
)
connection.send_error(
msg["id"],
const.ERR_HOME_ASSISTANT_ERROR,

View File

@@ -94,21 +94,59 @@ def _get_obj_holidays(
language=language,
categories=set_categories,
)
supported_languages = obj_holidays.supported_languages
default_language = obj_holidays.default_language
if default_language and not language:
# If no language is set, use the default language
LOGGER.debug("Changing language from None to %s", default_language)
return country_holidays( # Return default if no language
country,
subdiv=province,
years=year,
language=default_language,
categories=set_categories,
)
if (
(supported_languages := obj_holidays.supported_languages)
default_language
and language
and language not in supported_languages
and language.startswith("en")
):
# If language does not match supported languages, use the first English variant
if default_language.startswith("en"):
LOGGER.debug("Changing language from %s to %s", language, default_language)
return country_holidays( # Return default English if default language
country,
subdiv=province,
years=year,
language=default_language,
categories=set_categories,
)
for lang in supported_languages:
if lang.startswith("en"):
obj_holidays = country_holidays(
LOGGER.debug("Changing language from %s to %s", language, lang)
return country_holidays(
country,
subdiv=province,
years=year,
language=lang,
categories=set_categories,
)
LOGGER.debug("Changing language from %s to %s", language, lang)
if default_language and language and language not in supported_languages:
# If language does not match supported languages, use the default language
LOGGER.debug("Changing language from %s to %s", language, default_language)
return country_holidays( # Return default English if default language
country,
subdiv=province,
years=year,
language=default_language,
categories=set_categories,
)
return obj_holidays

View File

@@ -67,8 +67,7 @@ def add_province_and_language_to_schema(
_country = country_holidays(country=country)
if country_default_language := (_country.default_language):
selectable_languages = _country.supported_languages
new_selectable_languages = list(selectable_languages)
new_selectable_languages = list(_country.supported_languages)
language_schema = {
vol.Optional(
CONF_LANGUAGE, default=country_default_language
@@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None:
years=year,
language=language,
)
if (
(supported_languages := obj_holidays.supported_languages)
and language
and language.startswith("en")
):
for lang in supported_languages:
if lang.startswith("en"):
obj_holidays = country_holidays(
country,
subdiv=province,
years=year,
language=lang,
)
else:
obj_holidays = HolidayBase(years=year)

View File

@@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, api.mac)},
manufacturer=api.brand,
name=f"{api.network_name}",
name=api.network_name,
model="Zimi Cloud Connect",
sw_version=api.firmware_version,
connections={(CONNECTION_NETWORK_MAC, api.mac)},

View File

@@ -32,7 +32,7 @@ async def async_setup_entry(
]
lights.extend(
[ZimiDimmer(device, api) for device in api.lights if device.type == "dimmer"]
ZimiDimmer(device, api) for device in api.lights if device.type == "dimmer"
)
async_add_entities(lights)
@@ -81,8 +81,6 @@ class ZimiDimmer(ZimiLight):
super().__init__(device, api)
self._attr_color_mode = ColorMode.BRIGHTNESS
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
if self._device.type != "dimmer":
raise ValueError("ZimiDimmer needs a dimmable light")
async def async_turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on (with optional brightness)."""

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