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 }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image - name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile 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() entry.unique_id for entry in self._async_current_entries()
} }
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError): with suppress(TimeoutError):
async with timeout(5): async with timeout(5):
hubs: list[aiopulse.Hub] = [ hubs = [
hub hub
async for hub in aiopulse.Hub.discover() async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured if hub.id not in already_configured

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
) )
from . import AgentDVRConfigEntry 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) 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 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( self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Agent", manufacturer="Agent",
model="Camera", model="Camera",
name=f"{device.client.name} {device.name}", name=f"{device.client.name} {device.name}",

View File

@@ -5,23 +5,22 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from airthings import Airthings, AirthingsDevice, AirthingsError from airthings import Airthings
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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__) _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6) SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]] type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool: 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_get_clientsession(hass),
) )
async def _update_method() -> dict[str, AirthingsDevice]: coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
"""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 = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator 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, SIGNAL_STRENGTH_DECIBELS,
EntityCategory, EntityCategory,
UnitOfPressure, UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant 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.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType from . import AirthingsConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator
SENSORS: dict[str, SensorEntityDescription] = { SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription( "radonShortTermAvg": SensorEntityDescription(
@@ -54,6 +56,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
native_unit_of_measurement=UnitOfPressure.MBAR, native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT, 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( "battery": SensorEntityDescription(
key="battery", key="battery",
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
@@ -140,7 +148,7 @@ async def async_setup_entry(
class AirthingsHeaterEnergySensor( class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
): ):
"""Representation of a Airthings Sensor device.""" """Representation of a Airthings Sensor device."""
@@ -149,7 +157,7 @@ class AirthingsHeaterEnergySensor(
def __init__( def __init__(
self, self,
coordinator: AirthingsDataCoordinatorType, coordinator: AirthingsDataUpdateCoordinator,
airthings_device: AirthingsDevice, airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription, entity_description: SensorEntityDescription,
) -> None: ) -> None:

View File

@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(), ): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): 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"], "codeowners": ["@chemelli74"],
"config_flow": true, "config_flow": true,
"dhcp": [ "dhcp": [
{ "macaddress": "007147*" },
{ "macaddress": "00FC8B*" },
{ "macaddress": "0812A5*" },
{ "macaddress": "086AE5*" },
{ "macaddress": "08849D*" },
{ "macaddress": "089115*" },
{ "macaddress": "08A6BC*" }, { "macaddress": "08A6BC*" },
{ "macaddress": "08C224*" },
{ "macaddress": "0CDC91*" },
{ "macaddress": "0CEE99*" },
{ "macaddress": "1009F9*" },
{ "macaddress": "109693*" },
{ "macaddress": "10BF67*" }, { "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": "440049*" },
{ "macaddress": "40A2DB*" },
{ "macaddress": "40A9CF*" },
{ "macaddress": "40B4CD*" },
{ "macaddress": "443D54*" }, { "macaddress": "443D54*" },
{ "macaddress": "44650D*" },
{ "macaddress": "485F2D*" },
{ "macaddress": "48785E*" },
{ "macaddress": "48B423*" }, { "macaddress": "48B423*" },
{ "macaddress": "4C1744*" }, { "macaddress": "4C1744*" },
{ "macaddress": "4CEFC0*" },
{ "macaddress": "5007C3*" },
{ "macaddress": "50D45C*" }, { "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" }, { "macaddress": "50DCE7*" },
{ "macaddress": "50F5DA*" },
{ "macaddress": "5C415A*" },
{ "macaddress": "6837E9*" },
{ "macaddress": "6854FD*" },
{ "macaddress": "689A87*" },
{ "macaddress": "68B691*" },
{ "macaddress": "68DBF5*" },
{ "macaddress": "68F63B*" }, { "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "6C5697*" },
{ "macaddress": "7458F3*" },
{ "macaddress": "74C246*" },
{ "macaddress": "74D637*" }, { "macaddress": "74D637*" },
{ "macaddress": "74E20C*" },
{ "macaddress": "74ECB2*" },
{ "macaddress": "786C84*" },
{ "macaddress": "78A03F*" },
{ "macaddress": "7C6166*" }, { "macaddress": "7C6166*" },
{ "macaddress": "7C6305*" },
{ "macaddress": "7CD566*" },
{ "macaddress": "8871E5*" },
{ "macaddress": "901195*" }, { "macaddress": "901195*" },
{ "macaddress": "90235B*" },
{ "macaddress": "90A822*" },
{ "macaddress": "90F82E*" },
{ "macaddress": "943A91*" }, { "macaddress": "943A91*" },
{ "macaddress": "98226E*" }, { "macaddress": "98226E*" },
{ "macaddress": "98CCF3*" },
{ "macaddress": "9CC8E9*" }, { "macaddress": "9CC8E9*" },
{ "macaddress": "A002DC*" },
{ "macaddress": "A0D2B1*" },
{ "macaddress": "A40801*" },
{ "macaddress": "A8E621*" }, { "macaddress": "A8E621*" },
{ "macaddress": "AC416A*" },
{ "macaddress": "AC63BE*" },
{ "macaddress": "ACCCFC*" },
{ "macaddress": "B0739C*" },
{ "macaddress": "B0CFCB*" },
{ "macaddress": "B0F7C4*" },
{ "macaddress": "B85F98*" },
{ "macaddress": "C091B9*" },
{ "macaddress": "C095CF*" }, { "macaddress": "C095CF*" },
{ "macaddress": "C49500*" },
{ "macaddress": "C86C3D*" },
{ "macaddress": "CC9EA2*" },
{ "macaddress": "CCF735*" },
{ "macaddress": "DC54D7*" },
{ "macaddress": "D8BE65*" }, { "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", "documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub", "integration_type": "hub",

View File

@@ -5,7 +5,7 @@
"data_description_country": "The country of your Amazon account.", "data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address 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_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": { "config": {
"flow_title": "{username}", "flow_title": "{username}",

View File

@@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyaprilaire"], "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.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send 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 .config import AxisConfig
from .entity_loader import AxisEntityLoader from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource from .event_source import AxisEventSource
@@ -79,7 +79,7 @@ class AxisHub:
config_entry_id=self.config.entry.entry_id, config_entry_id=self.config.entry.entry_id,
configuration_url=self.api.config.url, configuration_url=self.api.config.url,
connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
identifiers={(AXIS_DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer=ATTR_MANUFACTURER, manufacturer=ATTR_MANUFACTURER,
model=f"{self.config.model} {self.product_type}", model=f"{self.config.model} {self.product_type}",
name=self.config.name, name=self.config.name,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr 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 .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
from .device_trigger import ( from .device_trigger import (
CONF_BOTH_BUTTONS, CONF_BOTH_BUTTONS,
@@ -200,6 +200,6 @@ def async_describe_events(
} }
async_describe_event( 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.""" """The decora component."""
DOMAIN = "decora"

View File

@@ -21,7 +21,11 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
) )
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME 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 import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from . import DOMAIN
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -90,6 +94,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up an Decora switch.""" """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 = [] lights = []
for address, device_config in config[CONF_DEVICES].items(): for address, device_config in config[CONF_DEVICES].items():
device = {} device = {}

View File

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

View File

@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity, ImageProcessingFaceEntity,
) )
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Dlib Face detection platform.""" """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] source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities( add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))

View File

@@ -1 +1,4 @@
"""The dlib_face_identify component.""" """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, ImageProcessingFaceEntity,
) )
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE 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 import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{ {
@@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Dlib Face detection platform.""" """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] confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES] faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE] source: list[dict[str, str]] = config[CONF_SOURCE]

View File

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

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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_IMPERIAL,
UNITS_METRIC, UNITS_METRIC,
) )
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema( RECONFIGURE_SCHEMA = vol.Schema(
{ {
@@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN], user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION], user_input[CONF_DESTINATION],
) )
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException: except InvalidApiKeyException:
return {"base": "invalid_auth"} return {"base": "invalid_auth"}
except TimeoutError: except TimeoutError:

View File

@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden, Forbidden,
GatewayTimeout, GatewayTimeout,
GoogleAPIError, GoogleAPIError,
PermissionDenied,
Unauthorized, Unauthorized,
) )
from google.maps.routing_v2 import ( from google.maps.routing_v2 import (
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2 from google.type import latlng_pb2
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv 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 homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try: try:
formatted_coordinates = coordinates.split(",") formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates)) vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid): except (AttributeError, vol.Invalid):
return Waypoint(address=location) return Waypoint(address=location)
return Waypoint( return Waypoint(
location=Location( location=Location(
@@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes( await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)] 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: except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message) _LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error raise InvalidApiKeyException from unauthorized_error
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception): class UnknownException(Exception):
"""Unknown API Error.""" """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 typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions 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 ( from google.maps.routing_v2 import (
ComputeRoutesRequest, ComputeRoutesRequest,
Route, Route,
@@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM, TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_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__) _LOGGER = logging.getLogger(__name__)
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes( response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)] request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
) )
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0: if response is not None and len(response.routes) > 0:
self._route = 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: except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex) _LOGGER.error("Error getting travel time: %s", ex)
self._route = None self._route = None

View File

@@ -21,6 +21,7 @@
} }
}, },
"error": { "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%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers" "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 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 from .entity import GroupEntity
DEFAULT_NAME = "Sensor Group" DEFAULT_NAME = "Sensor Group"
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return state_classes[0] return state_classes[0]
async_create_issue( async_create_issue(
self.hass, self.hass,
GROUP_DOMAIN, DOMAIN,
f"{self.entity_id}_state_classes_not_matching", f"{self.entity_id}_state_classes_not_matching",
is_fixable=False, is_fixable=False,
is_persistent=False, is_persistent=False,
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return device_classes[0] return device_classes[0]
async_create_issue( async_create_issue(
self.hass, self.hass,
GROUP_DOMAIN, DOMAIN,
f"{self.entity_id}_device_classes_not_matching", f"{self.entity_id}_device_classes_not_matching",
is_fixable=False, is_fixable=False,
is_persistent=False, is_persistent=False,
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
if device_class: if device_class:
async_create_issue( async_create_issue(
self.hass, self.hass,
GROUP_DOMAIN, DOMAIN,
f"{self.entity_id}_uoms_not_matching_device_class", f"{self.entity_id}_uoms_not_matching_device_class",
is_fixable=False, is_fixable=False,
is_persistent=False, is_persistent=False,
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
else: else:
async_create_issue( async_create_issue(
self.hass, self.hass,
GROUP_DOMAIN, DOMAIN,
f"{self.entity_id}_uoms_not_matching_no_device_class", f"{self.entity_id}_uoms_not_matching_no_device_class",
is_fixable=False, is_fixable=False,
is_persistent=False, is_persistent=False,

View File

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

View File

@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url, async_process_play_media_url,
) )
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP 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 import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline" CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Gstreamer platform.""" """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) name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE) pipeline = config.get(CONF_PIPELINE)

View File

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

View File

@@ -18,6 +18,10 @@
"title": "The {integration_title} YAML configuration is being removed", "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." "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": { "deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed", "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." "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", "iot_class": "local_polling",
"loggers": ["aioimmich"], "loggers": ["aioimmich"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["aioimmich==0.6.0"] "requirements": ["aioimmich==0.7.0"]
} }

View File

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

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from typing import Any 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_NAME, DOMAIN from .const import DOMAIN
from .coordinator import JellyfinDataUpdateCoordinator from .coordinator import JellyfinDataUpdateCoordinator
@@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
"""Initialize the Jellyfin entity.""" """Initialize the Jellyfin entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)}, 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_MUTE,
SERVICE_VOLUME_UP, 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 import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard" DOMAIN = "keyboard"
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events.""" """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 = PyKeyboard()
keyboard.special_key_assignment() keyboard.special_key_assignment()

View File

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

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylamarzocco"], "loggers": ["pylamarzocco"],
"quality_scale": "platinum", "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", key="prebrew_on",
translation_key="prebrew_time_on", translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=0, native_min_value=0,
native_max_value=10, native_max_value=10,
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off", key="prebrew_off",
translation_key="prebrew_time_off", translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=0, native_min_value=0,
native_max_value=10, native_max_value=10,

View File

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

View File

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

View File

@@ -7,8 +7,9 @@ import time
import lirc import lirc
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP 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 import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIRC capability.""" """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) # blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully # also by not blocking, we allow hass to shut down the thread gracefully
# on exit. # on exit.

View File

@@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty # don't discover this entry if the supported state list is empty
secondary_value_is_not=[], 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( MatterDiscoverySchema(
platform=Platform.SENSOR, platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription( entity_description=MatterSensorEntityDescription(
key="TargetPositionLiftPercent100ths", key="TargetPositionLiftPercent100ths",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
translation_key="window_covering_target_position", translation_key="window_covering_target_position",
measurement_to_ha=lambda x: round((10000 - x) / 100), measurement_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,

View File

@@ -390,12 +390,6 @@
"evse_user_max_charge_current": { "evse_user_max_charge_current": {
"name": "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": { "window_covering_target_position": {
"name": "Target opening 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": { "entity": {
"sensor": { "sensor": {
"room_temperature": { "room_temperature": {

View File

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

View File

@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS, DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
STATE_CLASS_UNITS,
STATE_CLASSES_SCHEMA, STATE_CLASSES_SCHEMA,
RestoreSensor, RestoreSensor,
SensorDeviceClass, SensorDeviceClass,
@@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'" 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 ( if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None: ) is None:

View File

@@ -644,6 +644,7 @@
"invalid_template": "Invalid template", "invalid_template": "Invalid template",
"invalid_supported_color_modes": "Invalid supported color modes selection", "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": "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", "invalid_url": "Invalid URL",
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only", "last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value", "max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",

View File

@@ -58,15 +58,3 @@ class NikoHomeControlConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors 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 typing import Any
from nhc.light import NHCLight from nhc.light import NHCLight
import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode, ColorMode,
LightEntity, LightEntity,
brightness_supported, brightness_supported,
) )
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_HOST from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
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 . import NHCController, NikoHomeControlConfigEntry from . import NHCController, NikoHomeControlConfigEntry
from .const import DOMAIN
from .entity import NikoHomeControlEntity 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -17,11 +17,5 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "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] PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
MIN_TEMPERATURE = 7 MIN_TEMPERATURE = 7
MAX_TEMPERATURE = 40 MAX_TEMPERATURE = 30
async def async_setup_entry( 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 homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NukiEntryData 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 .entity import NukiEntity
from .helpers import CannotConnect from .helpers import CannotConnect
@@ -29,7 +29,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up the Nuki lock platform.""" """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 coordinator = entry_data.coordinator
entities: list[NukiDeviceEntity] = [ 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback 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 .entity import OneWireEntity, OneWireEntityDescription
from .onewirehub import ( from .onewirehub import (
SIGNAL_NEW_DEVICE_CONNECTED, SIGNAL_NEW_DEVICE_CONNECTED,
@@ -37,13 +37,14 @@ class OneWireBinarySensorEntityDescription(
): ):
"""Class describing OneWire binary sensor entities.""" """Class describing OneWire binary sensor entities."""
read_mode = READ_MODE_INT
DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
"12": tuple( "12": tuple(
OneWireBinarySensorEntityDescription( OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}", key=f"sensed.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id", translation_key="sensed_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -53,7 +54,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
OneWireBinarySensorEntityDescription( OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}", key=f"sensed.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id", translation_key="sensed_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -63,7 +63,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
OneWireBinarySensorEntityDescription( OneWireBinarySensorEntityDescription(
key=f"sensed.{device_key}", key=f"sensed.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="sensed_id", translation_key="sensed_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -78,7 +77,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
OneWireBinarySensorEntityDescription( OneWireBinarySensorEntityDescription(
key=f"hub/short.{device_key}", key=f"hub/short.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
translation_key="hub_short_id", translation_key="hub_short_id",
@@ -162,4 +160,4 @@ class OneWireBinarySensorEntity(OneWireEntity, BinarySensorEntity):
"""Return true if sensor is on.""" """Return true if sensor is on."""
if self._state is None: if self._state is None:
return 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_HOBBYBOARDS = "Hobby Boards"
MANUFACTURER_EDS = "Embedded Data Systems" MANUFACTURER_EDS = "Embedded Data Systems"
READ_MODE_BOOL = "bool"
READ_MODE_FLOAT = "float" READ_MODE_FLOAT = "float"
READ_MODE_INT = "int" READ_MODE_INT = "int"

View File

@@ -10,9 +10,8 @@ from pyownet import protocol
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription 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) @dataclass(frozen=True)
@@ -45,7 +44,7 @@ class OneWireEntity(Entity):
self._attr_unique_id = f"/{device_id}/{description.key}" self._attr_unique_id = f"/{device_id}/{description.key}"
self._attr_device_info = device_info self._attr_device_info = device_info
self._device_file = device_file self._device_file = device_file
self._state: StateType = None self._state: int | float | None = None
self._value_raw: float | None = None self._value_raw: float | None = None
self._owproxy = owproxy self._owproxy = owproxy
@@ -82,7 +81,5 @@ class OneWireEntity(Entity):
_LOGGER.debug("Fetching %s data recovered", self.name) _LOGGER.debug("Fetching %s data recovered", self.name)
if self.entity_description.read_mode == READ_MODE_INT: if self.entity_description.read_mode == READ_MODE_INT:
self._state = int(self._value_raw) self._state = int(self._value_raw)
elif self.entity_description.read_mode == READ_MODE_BOOL:
self._state = int(self._value_raw) == 1
else: else:
self._state = self._value_raw 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback 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 .entity import OneWireEntity, OneWireEntityDescription
from .onewirehub import ( from .onewirehub import (
SIGNAL_NEW_DEVICE_CONNECTED, SIGNAL_NEW_DEVICE_CONNECTED,
@@ -32,13 +32,14 @@ SCAN_INTERVAL = timedelta(seconds=30)
class OneWireSwitchEntityDescription(OneWireEntityDescription, SwitchEntityDescription): class OneWireSwitchEntityDescription(OneWireEntityDescription, SwitchEntityDescription):
"""Class describing OneWire switch entities.""" """Class describing OneWire switch entities."""
read_mode = READ_MODE_INT
DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
"05": ( "05": (
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key="PIO", key="PIO",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio", translation_key="pio",
), ),
), ),
@@ -47,7 +48,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"PIO.{device_key}", key=f"PIO.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id", translation_key="pio_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -57,7 +57,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"latch.{device_key}", key=f"latch.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="latch_id", translation_key="latch_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -69,7 +68,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key="IAD", key="IAD",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
read_mode=READ_MODE_BOOL,
translation_key="iad", translation_key="iad",
), ),
), ),
@@ -78,7 +76,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"PIO.{device_key}", key=f"PIO.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id", translation_key="pio_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -88,7 +85,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"latch.{device_key}", key=f"latch.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="latch_id", translation_key="latch_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -99,7 +95,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"PIO.{device_key}", key=f"PIO.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
translation_key="pio_id", translation_key="pio_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
) )
@@ -115,7 +110,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"hub/branch.{device_key}", key=f"hub/branch.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="hub_branch_id", translation_key="hub_branch_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
@@ -127,7 +121,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"moisture/is_leaf.{device_key}", key=f"moisture/is_leaf.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="leaf_sensor_id", translation_key="leaf_sensor_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
@@ -138,7 +131,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
OneWireSwitchEntityDescription( OneWireSwitchEntityDescription(
key=f"moisture/is_moisture.{device_key}", key=f"moisture/is_moisture.{device_key}",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="moisture_sensor_id", translation_key="moisture_sensor_id",
translation_placeholders={"id": str(device_key)}, translation_placeholders={"id": str(device_key)},
@@ -226,7 +218,7 @@ class OneWireSwitchEntity(OneWireEntity, SwitchEntity):
"""Return true if switch is on.""" """Return true if switch is on."""
if self._state is None: if self._state is None:
return None return None
return bool(self._state) return self._state == 1
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""

View File

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

View File

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

View File

@@ -27,10 +27,13 @@ from homeassistant.const import (
SERVICE_VOLUME_DOWN, SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_UP, 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -53,6 +56,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Pandora media player platform.""" """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(): if not _pianobar_exists():
return return
pandora = PandoraMediaPlayer("Pandora") pandora = PandoraMediaPlayer("Pandora")

View File

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

View File

@@ -7,7 +7,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": [ "requirements": [
"SQLAlchemy==2.0.40", "SQLAlchemy==2.0.41",
"fnv-hash-fast==1.5.0", "fnv-hash-fast==1.5.0",
"psutil-home-assistant==0.0.1" "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 "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) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload( config_entry.async_on_unload(

View File

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

View File

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

View File

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

View File

@@ -216,6 +216,16 @@ SWITCH_ENTITIES = (
value=lambda api, ch: api.baichuan.privacy_mode(ch), value=lambda api, ch: api.baichuan.privacy_mode(ch),
method=lambda api, ch, value: api.baichuan.set_privacy_mode(ch, value), 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 = ( NVR_SWITCH_ENTITIES = (

View File

@@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView):
verify_ssl=False, verify_ssl=False,
ssl_cipher=SSLCipherList.INSECURE, ssl_cipher=SSLCipherList.INSECURE,
) )
self._vod_type: str | None = None
async def get( async def get(
self, self,
@@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView):
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8") filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
ch = int(channel) ch = int(channel)
if self._vod_type is not None:
vod_type = self._vod_type
try: try:
host = get_host(self.hass, config_entry_id) host = get_host(self.hass, config_entry_id)
except Unresolvable: except Unresolvable:
@@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView):
"apolication/octet-stream", "apolication/octet-stream",
]: ]:
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}" 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) _LOGGER.error(err_str)
if reolink_response.content_type == "text/html": if reolink_response.content_type == "text/html":
text = await reolink_response.text() text = await reolink_response.text()
@@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView):
reolink_response.reason, reolink_response.reason,
response_headers, 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( response = web.StreamResponse(
status=reolink_response.status, 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.""" """Mark the first item with matching `name` as completed."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
name = call.data[ATTR_NAME] name = call.data[ATTR_NAME]
try: try:
item = [item for item in data.items if item["name"] == name][0] await data.async_complete(name)
except IndexError: except NoMatchingShoppingListItem:
_LOGGER.error("Updating of item failed: %s cannot be found", name) _LOGGER.error("Completing of item failed: %s cannot be found", name)
else:
await data.async_update(item["id"], {"name": name, "complete": True})
async def incomplete_item_service(call: ServiceCall) -> None: async def incomplete_item_service(call: ServiceCall) -> None:
"""Mark the first item with matching `name` as incomplete.""" """Mark the first item with matching `name` as incomplete."""
@@ -258,6 +255,30 @@ class ShoppingData:
) )
return removed 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( async def async_update(
self, item_id: str | None, info: dict[str, Any], context: Context | None = None self, item_id: str | None, info: dict[str, Any], context: Context | None = None
) -> dict[str, JsonValueType]: ) -> dict[str, JsonValueType]:

View File

@@ -5,15 +5,17 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, intent 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_ADD_ITEM = "HassShoppingListAddItem"
INTENT_COMPLETE_ITEM = "HassShoppingListCompleteItem"
INTENT_LAST_ITEMS = "HassShoppingListLastItems" INTENT_LAST_ITEMS = "HassShoppingListLastItems"
async def async_setup_intents(hass: HomeAssistant) -> None: async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the Shopping List intents.""" """Set up the Shopping List intents."""
intent.async_register(hass, AddItemIntent()) intent.async_register(hass, AddItemIntent())
intent.async_register(hass, CompleteItemIntent())
intent.async_register(hass, ListTopItemsIntent()) intent.async_register(hass, ListTopItemsIntent())
@@ -36,6 +38,33 @@ class AddItemIntent(intent.IntentHandler):
return response 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): class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents.""" """Handle AddItem intents."""
@@ -47,7 +76,7 @@ class ListTopItemsIntent(intent.IntentHandler):
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent.""" """Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:] items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response() response: intent.IntentResponse = intent_obj.create_response()
if not items: if not items:
response.async_set_speech("There are no items on your shopping list") response.async_set_speech("There are no items on your shopping list")

View File

@@ -1,25 +1,37 @@
"""Common base for entities.""" """Common base for entities."""
from dataclasses import dataclass
from typing import Any from typing import Any
from pysmarlaapi import Federwiege from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.helpers.device_registry import DeviceInfo 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 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): class SmarlaBaseEntity(Entity):
"""Common Base Entity class for defining Smarla device.""" """Common Base Entity class for defining Smarla device."""
entity_description: SmarlaEntityDescription
_attr_should_poll = False _attr_should_poll = False
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__(self, federwiege: Federwiege, prop: Property) -> None: def __init__(self, federwiege: Federwiege, desc: SmarlaEntityDescription) -> None:
"""Initialise the entity.""" """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( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, federwiege.serial_number)}, identifiers={(DOMAIN, federwiege.serial_number)},
name=DEVICE_MODEL_NAME, name=DEVICE_MODEL_NAME,

View File

@@ -3,7 +3,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property from pysmarlaapi.federwiege.classes import Property
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
@@ -11,16 +10,13 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FederwiegeConfigEntry from . import FederwiegeConfigEntry
from .entity import SmarlaBaseEntity from .entity import SmarlaBaseEntity, SmarlaEntityDescription
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class SmarlaSwitchEntityDescription(SwitchEntityDescription): class SmarlaSwitchEntityDescription(SmarlaEntityDescription, SwitchEntityDescription):
"""Class describing Swing2Sleep Smarla switch entity.""" """Class describing Swing2Sleep Smarla switch entity."""
service: str
property: str
SWITCHES: list[SmarlaSwitchEntityDescription] = [ SWITCHES: list[SmarlaSwitchEntityDescription] = [
SmarlaSwitchEntityDescription( SmarlaSwitchEntityDescription(
@@ -55,17 +51,6 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
_property: Property[bool] _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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the entity value to represent the entity state.""" """Return the entity value to represent the entity state."""

View File

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

View File

@@ -6,9 +6,14 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform 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.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery 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 homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
@@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure Gammu state machine.""" """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] device = entry.data[CONF_DEVICE]
connection_mode = "at" 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] gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
await gateway.terminate_async() 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 return unload_ok

View File

@@ -7,8 +7,13 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components import mqtt 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 import config_validation as cv, intent
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
DOMAIN = "snips" DOMAIN = "snips"
@@ -91,6 +96,20 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate Snips component.""" """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 # Make sure MQTT integration is enabled and the client is available
if not await mqtt.async_wait_for_mqtt_client(hass): if not await mqtt.async_wait_for_mqtt_client(hass):

View File

@@ -6,5 +6,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql", "documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling", "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.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN as SWITCH_DOMAIN from .const import DOMAIN
DEFAULT_NAME = "Light Switch" DEFAULT_NAME = "Light Switch"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Forward the turn_on command to the switch in this light switch.""" """Forward the turn_on command to the switch in this light switch."""
await self.hass.services.async_call( await self.hass.services.async_call(
SWITCH_DOMAIN, DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self._switch_entity_id}, {ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True, blocking=True,
@@ -86,7 +86,7 @@ class LightSwitch(LightEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Forward the turn_off command to the switch in this light switch.""" """Forward the turn_off command to the switch in this light switch."""
await self.hass.services.async_call( await self.hass.services.async_call(
SWITCH_DOMAIN, DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self._switch_entity_id}, {ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True, 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.entity import Entity, ToggleEntity
from homeassistant.helpers.event import async_track_state_change_event 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): class BaseEntity(Entity):
@@ -61,7 +61,7 @@ class BaseEntity(Entity):
self._switch_entity_id = switch_entity_id self._switch_entity_id = switch_entity_id
self._is_new_entity = ( 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 @callback
@@ -102,7 +102,7 @@ class BaseEntity(Entity):
if registry.async_get(self.entity_id) is not None: if registry.async_get(self.entity_id) is not None:
registry.async_update_entity_options( registry.async_update_entity_options(
self.entity_id, self.entity_id,
SWITCH_AS_X_DOMAIN, DOMAIN,
self.async_generate_entity_options(), self.async_generate_entity_options(),
) )

View File

@@ -7,7 +7,13 @@ from dataclasses import dataclass, field
from logging import getLogger from logging import getLogger
from aiohttp import web 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.components import webhook
from homeassistant.config_entries import ConfigEntry 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) api = SwitchBotAPI(token=token, secret=secret)
try: try:
devices = await api.list_devices() devices = await api.list_devices()
except InvalidAuth as ex: except SwitchBotAuthenticationError as ex:
_LOGGER.error( _LOGGER.error(
"Invalid authentication while connecting to SwitchBot API: %s", ex "Invalid authentication while connecting to SwitchBot API: %s", ex
) )
return False return False
except CannotConnect as ex: except SwitchBotConnectionError as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
_LOGGER.debug("Devices: %s", devices) _LOGGER.debug("Devices: %s", devices)
coordinators_by_id: dict[str, SwitchBotCoordinator] = {} coordinators_by_id: dict[str, SwitchBotCoordinator] = {}

View File

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

View File

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

View File

@@ -8,5 +8,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["switchbot_api"], "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) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
SCAN_INTERVAL = timedelta(minutes=5) 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]]): class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):

View File

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

View File

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

View File

@@ -26,15 +26,21 @@ from homeassistant.const import (
CONF_SOURCE, CONF_SOURCE,
EVENT_HOMEASSISTANT_START, 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 import config_validation as cv, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.pil import draw_box from homeassistant.util.pil import draw_box
from . import CONF_GRAPH, DOMAIN
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
DOMAIN = "tensorflow"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_MATCHES = "matches" ATTR_MATCHES = "matches"
@@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom"
CONF_CATEGORIES = "categories" CONF_CATEGORIES = "categories"
CONF_CATEGORY = "category" CONF_CATEGORY = "category"
CONF_FILE_OUT = "file_out" CONF_FILE_OUT = "file_out"
CONF_GRAPH = "graph"
CONF_LABELS = "labels" CONF_LABELS = "labels"
CONF_LABEL_OFFSET = "label_offset" CONF_LABEL_OFFSET = "label_offset"
CONF_LEFT = "left" CONF_LEFT = "left"
@@ -110,6 +115,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the TensorFlow image processing platform.""" """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_config = config[CONF_MODEL]
model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow") model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow")
labels = model_config.get(CONF_LABELS) or hass.config.path( 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", "documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "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", key="charge_state_conn_charge_cable",
polling=True, polling=True,
polling_value_fn=lambda x: x != "<invalid>", polling_value_fn=lambda x: x != "<invalid>",
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType(
lambda value: callback(value != "Unknown")
),
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
), ),

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry", "documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "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", "documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"], "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, "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 PARALLEL_UPDATES = 0
@@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
"""Vehicle Location Media Class.""" """Vehicle Location Media Class."""
_attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_volume_step = VOLUME_STEP
def __init__( def __init__(
self, self,
@@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
@property @property
def volume_level(self) -> float: def volume_level(self) -> float:
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self.get("vehicle_state_media_info_audio_volume", 0) / self.get( return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR
"vehicle_state_media_info_audio_volume_max", 10.333333
)
@property @property
def media_duration(self) -> int | None: 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 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 .config import UnifiConfig
from .entity_helper import UnifiEntityHelper from .entity_helper import UnifiEntityHelper
from .entity_loader import UnifiEntityLoader from .entity_loader import UnifiEntityLoader
@@ -104,7 +104,7 @@ class UnifiHub:
return DeviceInfo( return DeviceInfo(
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
identifiers={(UNIFI_DOMAIN, self.config.entry.unique_id)}, identifiers={(DOMAIN, self.config.entry.unique_id)},
manufacturer=ATTR_MANUFACTURER, manufacturer=ATTR_MANUFACTURER,
model="UniFi Network Application", model="UniFi Network Application",
name="UniFi Network", 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 homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import UnifiConfigEntry from . import UnifiConfigEntry
from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .const import ATTR_MANUFACTURER, DOMAIN
from .entity import ( from .entity import (
HandlerT, HandlerT,
SubscriptionT, 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: def update_unique_id(obj_id: str, type_name: str) -> None:
"""Rework unique ID.""" """Rework unique ID."""
new_unique_id = f"{type_name}-{obj_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 return
prefix, _, suffix = obj_id.partition("_") prefix, _, suffix = obj_id.partition("_")
unique_id = f"{prefix}-{type_name}-{suffix}" unique_id = f"{prefix}-{type_name}-{suffix}"
if entity_id := ent_reg.async_get_entity_id( if entity_id := ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id):
SWITCH_DOMAIN, UNIFI_DOMAIN, unique_id
):
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
for obj_id in hub.api.outlets: for obj_id in hub.api.outlets:

View File

@@ -40,7 +40,7 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"], "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": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@@ -300,7 +300,9 @@ async def handle_call_service(
translation_placeholders=err.translation_placeholders, translation_placeholders=err.translation_placeholders,
) )
except HomeAssistantError as err: 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( connection.send_error(
msg["id"], msg["id"],
const.ERR_HOME_ASSISTANT_ERROR, const.ERR_HOME_ASSISTANT_ERROR,

View File

@@ -94,21 +94,59 @@ def _get_obj_holidays(
language=language, language=language,
categories=set_categories, 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 ( if (
(supported_languages := obj_holidays.supported_languages) default_language
and language and language
and language not in supported_languages
and language.startswith("en") 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: for lang in supported_languages:
if lang.startswith("en"): if lang.startswith("en"):
obj_holidays = country_holidays( LOGGER.debug("Changing language from %s to %s", language, lang)
return country_holidays(
country, country,
subdiv=province, subdiv=province,
years=year, years=year,
language=lang, language=lang,
categories=set_categories, 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 return obj_holidays

View File

@@ -67,8 +67,7 @@ def add_province_and_language_to_schema(
_country = country_holidays(country=country) _country = country_holidays(country=country)
if country_default_language := (_country.default_language): if country_default_language := (_country.default_language):
selectable_languages = _country.supported_languages new_selectable_languages = list(_country.supported_languages)
new_selectable_languages = list(selectable_languages)
language_schema = { language_schema = {
vol.Optional( vol.Optional(
CONF_LANGUAGE, default=country_default_language CONF_LANGUAGE, default=country_default_language
@@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None:
years=year, years=year,
language=language, 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: else:
obj_holidays = HolidayBase(years=year) 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, config_entry_id=entry.entry_id,
identifiers={(DOMAIN, api.mac)}, identifiers={(DOMAIN, api.mac)},
manufacturer=api.brand, manufacturer=api.brand,
name=f"{api.network_name}", name=api.network_name,
model="Zimi Cloud Connect", model="Zimi Cloud Connect",
sw_version=api.firmware_version, sw_version=api.firmware_version,
connections={(CONNECTION_NETWORK_MAC, api.mac)}, connections={(CONNECTION_NETWORK_MAC, api.mac)},

View File

@@ -32,7 +32,7 @@ async def async_setup_entry(
] ]
lights.extend( 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) async_add_entities(lights)
@@ -81,8 +81,6 @@ class ZimiDimmer(ZimiLight):
super().__init__(device, api) super().__init__(device, api)
self._attr_color_mode = ColorMode.BRIGHTNESS self._attr_color_mode = ColorMode.BRIGHTNESS
self._attr_supported_color_modes = {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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on (with optional brightness).""" """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