forked from home-assistant/core
Merge branch 'dev' into epenet-20250527-1510
This commit is contained in:
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
6
homeassistant/brands/shelly.json
Normal file
6
homeassistant/brands/shelly.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "shelly",
|
||||
"name": "shelly",
|
||||
"integrations": ["shelly"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(TimeoutError):
|
||||
async with timeout(5):
|
||||
hubs: list[aiopulse.Hub] = [
|
||||
hubs = [
|
||||
hub
|
||||
async for hub in aiopulse.Hub.discover()
|
||||
if hub.id not in already_configured
|
||||
|
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
)
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
|
||||
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
manufacturer="Agent",
|
||||
model="Camera",
|
||||
name=f"{device.client.name} {device.name}",
|
||||
|
@@ -5,23 +5,22 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SECRET, DOMAIN
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
@@ -32,21 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async def _update_method() -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
36
homeassistant/components/airthings/coordinator.py
Normal file
36
homeassistant/components/airthings/coordinator.py
Normal 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
|
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,8 +28,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
|
||||
from . import AirthingsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
|
||||
SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"radonShortTermAvg": SensorEntityDescription(
|
||||
@@ -54,6 +56,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"sla": SensorEntityDescription(
|
||||
key="sla",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
@@ -140,7 +148,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class AirthingsHeaterEnergySensor(
|
||||
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
|
||||
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Representation of a Airthings Sensor device."""
|
||||
|
||||
@@ -149,7 +157,7 @@ class AirthingsHeaterEnergySensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirthingsDataCoordinatorType,
|
||||
coordinator: AirthingsDataUpdateCoordinator,
|
||||
airthings_device: AirthingsDevice,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
|
@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.positive_int,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@@ -4,25 +4,114 @@
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "007147*" },
|
||||
{ "macaddress": "00FC8B*" },
|
||||
{ "macaddress": "0812A5*" },
|
||||
{ "macaddress": "086AE5*" },
|
||||
{ "macaddress": "08849D*" },
|
||||
{ "macaddress": "089115*" },
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "08C224*" },
|
||||
{ "macaddress": "0CDC91*" },
|
||||
{ "macaddress": "0CEE99*" },
|
||||
{ "macaddress": "1009F9*" },
|
||||
{ "macaddress": "109693*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "10CE02*" },
|
||||
{ "macaddress": "140AC5*" },
|
||||
{ "macaddress": "149138*" },
|
||||
{ "macaddress": "1848BE*" },
|
||||
{ "macaddress": "1C12B0*" },
|
||||
{ "macaddress": "1C4D66*" },
|
||||
{ "macaddress": "1C93C4*" },
|
||||
{ "macaddress": "1CFE2B*" },
|
||||
{ "macaddress": "244CE3*" },
|
||||
{ "macaddress": "24CE33*" },
|
||||
{ "macaddress": "2873F6*" },
|
||||
{ "macaddress": "2C71FF*" },
|
||||
{ "macaddress": "34AFB3*" },
|
||||
{ "macaddress": "34D270*" },
|
||||
{ "macaddress": "38F73D*" },
|
||||
{ "macaddress": "3C5CC4*" },
|
||||
{ "macaddress": "3CE441*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "40A2DB*" },
|
||||
{ "macaddress": "40A9CF*" },
|
||||
{ "macaddress": "40B4CD*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "44650D*" },
|
||||
{ "macaddress": "485F2D*" },
|
||||
{ "macaddress": "48785E*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "4CEFC0*" },
|
||||
{ "macaddress": "5007C3*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "50F5DA*" },
|
||||
{ "macaddress": "5C415A*" },
|
||||
{ "macaddress": "6837E9*" },
|
||||
{ "macaddress": "6854FD*" },
|
||||
{ "macaddress": "689A87*" },
|
||||
{ "macaddress": "68B691*" },
|
||||
{ "macaddress": "68DBF5*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "6C0C9A*" },
|
||||
{ "macaddress": "6C5697*" },
|
||||
{ "macaddress": "7458F3*" },
|
||||
{ "macaddress": "74C246*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "74E20C*" },
|
||||
{ "macaddress": "74ECB2*" },
|
||||
{ "macaddress": "786C84*" },
|
||||
{ "macaddress": "78A03F*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "7C6305*" },
|
||||
{ "macaddress": "7CD566*" },
|
||||
{ "macaddress": "8871E5*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "90235B*" },
|
||||
{ "macaddress": "90A822*" },
|
||||
{ "macaddress": "90F82E*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "98CCF3*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A002DC*" },
|
||||
{ "macaddress": "A0D2B1*" },
|
||||
{ "macaddress": "A40801*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "AC416A*" },
|
||||
{ "macaddress": "AC63BE*" },
|
||||
{ "macaddress": "ACCCFC*" },
|
||||
{ "macaddress": "B0739C*" },
|
||||
{ "macaddress": "B0CFCB*" },
|
||||
{ "macaddress": "B0F7C4*" },
|
||||
{ "macaddress": "B85F98*" },
|
||||
{ "macaddress": "C091B9*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "C49500*" },
|
||||
{ "macaddress": "C86C3D*" },
|
||||
{ "macaddress": "CC9EA2*" },
|
||||
{ "macaddress": "CCF735*" },
|
||||
{ "macaddress": "DC54D7*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "EC2BEB*" }
|
||||
{ "macaddress": "D8FBD6*" },
|
||||
{ "macaddress": "DC91BF*" },
|
||||
{ "macaddress": "DCA0D0*" },
|
||||
{ "macaddress": "E0F728*" },
|
||||
{ "macaddress": "EC2BEB*" },
|
||||
{ "macaddress": "EC8AC4*" },
|
||||
{ "macaddress": "ECA138*" },
|
||||
{ "macaddress": "F02F9E*" },
|
||||
{ "macaddress": "F0272D*" },
|
||||
{ "macaddress": "F0F0A4*" },
|
||||
{ "macaddress": "F4032A*" },
|
||||
{ "macaddress": "F854B8*" },
|
||||
{ "macaddress": "FC492D*" },
|
||||
{ "macaddress": "FC65DE*" },
|
||||
{ "macaddress": "FCA183*" },
|
||||
{ "macaddress": "FCE9D8*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password sent to your email address."
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
"requirements": ["pyaprilaire==0.9.1"]
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN
|
||||
from .config import AxisConfig
|
||||
from .entity_loader import AxisEntityLoader
|
||||
from .event_source import AxisEventSource
|
||||
@@ -79,7 +79,7 @@ class AxisHub:
|
||||
config_entry_id=self.config.entry.entry_id,
|
||||
configuration_url=self.api.config.url,
|
||||
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
|
||||
identifiers={(AXIS_DOMAIN, self.unique_id)},
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=f"{self.config.model} {self.product_type}",
|
||||
name=self.config.name,
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
|
||||
from .const import CONF_GESTURE, DOMAIN
|
||||
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
|
||||
from .device_trigger import (
|
||||
CONF_BOTH_BUTTONS,
|
||||
@@ -200,6 +200,6 @@ def async_describe_events(
|
||||
}
|
||||
|
||||
async_describe_event(
|
||||
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
)
|
||||
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
|
@@ -1 +1,3 @@
|
||||
"""The decora component."""
|
||||
|
||||
DOMAIN = "decora"
|
||||
|
@@ -21,7 +21,11 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -90,6 +94,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Decora switch."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Leviton Decora",
|
||||
},
|
||||
)
|
||||
|
||||
lights = []
|
||||
for address, device_config in config[CONF_DEVICES].items():
|
||||
device = {}
|
||||
|
@@ -1 +1,3 @@
|
||||
"""The dlib_face_detect component."""
|
||||
|
||||
DOMAIN = "dlib_face_detect"
|
||||
|
@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
||||
|
||||
|
||||
@@ -25,6 +32,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Detect",
|
||||
},
|
||||
)
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
add_entities(
|
||||
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
||||
|
@@ -1 +1,4 @@
|
||||
"""The dlib_face_identify component."""
|
||||
|
||||
CONF_FACES = "faces"
|
||||
DOMAIN = "dlib_face_identify"
|
||||
|
@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_FACES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FACES = "faces"
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@@ -39,6 +45,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Identify",
|
||||
},
|
||||
)
|
||||
|
||||
confidence: float = config[CONF_CONFIDENCE]
|
||||
faces: dict[str, str] = config[CONF_FACES]
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -62,16 +62,16 @@ async def async_validate_hostname(
|
||||
"""Validate hostname."""
|
||||
|
||||
async def async_check(
|
||||
hostname: str, resolver: str, qtype: str, port: int = 53
|
||||
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
|
||||
) -> bool:
|
||||
"""Return if able to resolve hostname."""
|
||||
result = False
|
||||
result: bool = False
|
||||
with contextlib.suppress(DNSError):
|
||||
result = bool(
|
||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
).query(hostname, qtype)
|
||||
_resolver = aiodns.DNSResolver(
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
)
|
||||
result = bool(await _resolver.query(hostname, qtype))
|
||||
|
||||
return result
|
||||
|
||||
result: dict[str, bool] = {}
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250526.0"]
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
}
|
||||
|
@@ -50,7 +50,12 @@ from .const import (
|
||||
UNITS_IMPERIAL,
|
||||
UNITS_METRIC,
|
||||
)
|
||||
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
|
||||
from .helpers import (
|
||||
InvalidApiKeyException,
|
||||
PermissionDeniedException,
|
||||
UnknownException,
|
||||
validate_config_entry,
|
||||
)
|
||||
|
||||
RECONFIGURE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -188,6 +193,8 @@ async def validate_input(
|
||||
user_input[CONF_ORIGIN],
|
||||
user_input[CONF_DESTINATION],
|
||||
)
|
||||
except PermissionDeniedException:
|
||||
return {"base": "permission_denied"}
|
||||
except InvalidApiKeyException:
|
||||
return {"base": "invalid_auth"}
|
||||
except TimeoutError:
|
||||
|
@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
|
||||
Forbidden,
|
||||
GatewayTimeout,
|
||||
GoogleAPIError,
|
||||
PermissionDenied,
|
||||
Unauthorized,
|
||||
)
|
||||
from google.maps.routing_v2 import (
|
||||
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
|
||||
from google.type import latlng_pb2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
|
||||
try:
|
||||
formatted_coordinates = coordinates.split(",")
|
||||
vol.Schema(cv.gps(formatted_coordinates))
|
||||
except (AttributeError, vol.ExactSequenceInvalid):
|
||||
except (AttributeError, vol.Invalid):
|
||||
return Waypoint(address=location)
|
||||
return Waypoint(
|
||||
location=Location(
|
||||
@@ -67,6 +76,9 @@ async def validate_config_entry(
|
||||
await client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", field_mask)]
|
||||
)
|
||||
except PermissionDenied as permission_error:
|
||||
_LOGGER.error("Permission denied: %s", permission_error.message)
|
||||
raise PermissionDeniedException from permission_error
|
||||
except (Unauthorized, Forbidden) as unauthorized_error:
|
||||
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
||||
raise InvalidApiKeyException from unauthorized_error
|
||||
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
|
||||
|
||||
class UnknownException(Exception):
|
||||
"""Unknown API Error."""
|
||||
|
||||
|
||||
class PermissionDeniedException(Exception):
|
||||
"""Permission Denied Error."""
|
||||
|
||||
|
||||
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Create an issue for the Routes API being disabled."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"routes_api_disabled_{entry.entry_id}",
|
||||
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="routes_api_disabled",
|
||||
translation_placeholders={
|
||||
"entry_title": entry.title,
|
||||
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
|
||||
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Delete the issue for the Routes API being disabled."""
|
||||
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")
|
||||
|
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import GoogleAPIError
|
||||
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
|
||||
from google.maps.routing_v2 import (
|
||||
ComputeRoutesRequest,
|
||||
Route,
|
||||
@@ -58,7 +58,11 @@ from .const import (
|
||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
||||
UNITS_TO_GOOGLE_SDK_ENUM,
|
||||
)
|
||||
from .helpers import convert_to_waypoint
|
||||
from .helpers import (
|
||||
convert_to_waypoint,
|
||||
create_routes_api_disabled_issue,
|
||||
delete_routes_api_disabled_issue,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
response = await self._client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
||||
)
|
||||
_LOGGER.debug("Received response: %s", response)
|
||||
if response is not None and len(response.routes) > 0:
|
||||
self._route = response.routes[0]
|
||||
delete_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
except PermissionDenied:
|
||||
_LOGGER.error("Routes API is disabled for this API key")
|
||||
create_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
self._route = None
|
||||
except GoogleAPIError as ex:
|
||||
_LOGGER.error("Error getting travel time: %s", ex)
|
||||
self._route = None
|
||||
|
@@ -21,6 +21,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||
@@ -100,5 +101,11 @@
|
||||
"fewer_transfers": "Fewer transfers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"routes_api_disabled": {
|
||||
"title": "The Routes API must be enabled",
|
||||
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .entity import GroupEntity
|
||||
|
||||
DEFAULT_NAME = "Sensor Group"
|
||||
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return state_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_state_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return device_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_device_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if device_class:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
else:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_no_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
|
@@ -1 +1,3 @@
|
||||
"""The gstreamer component."""
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PIPELINE = "pipeline"
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
|
||||
@@ -48,6 +50,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Gstreamer platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "GStreamer",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
pipeline = config.get(CONF_PIPELINE)
|
||||
|
@@ -10,17 +10,17 @@
|
||||
"macaddress": "C8D778*"
|
||||
},
|
||||
{
|
||||
"hostname": "(bosch|siemens)-*",
|
||||
"hostname": "(balay|bosch|neff|siemens)-*",
|
||||
"macaddress": "68A40E*"
|
||||
},
|
||||
{
|
||||
"hostname": "siemens-*",
|
||||
"hostname": "(siemens|neff)-*",
|
||||
"macaddress": "38B4D3*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
@@ -18,6 +18,10 @@
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_config_flow_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_yaml_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.6.0"]
|
||||
"requirements": ["aioimmich==0.7.0"]
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
import mimetypes
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||
from aioimmich.exceptions import ImmichError
|
||||
@@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
"""Set up Immich media source."""
|
||||
entries = hass.config_entries.async_entries(
|
||||
DOMAIN, include_disabled=False, include_ignore=False
|
||||
)
|
||||
hass.http.register_view(ImmichMediaView(hass))
|
||||
return ImmichMediaSource(hass, entries)
|
||||
return ImmichMediaSource(hass)
|
||||
|
||||
|
||||
class ImmichMediaSourceIdentifier:
|
||||
@@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
|
||||
|
||||
def __init__(self, identifier: str) -> None:
|
||||
"""Split identifier into parts."""
|
||||
parts = identifier.split("/")
|
||||
# coonfig_entry.unique_id/album_id/asset_it/filename
|
||||
parts = identifier.split("|")
|
||||
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
|
||||
self.unique_id = parts[0]
|
||||
self.album_id = parts[1] if len(parts) > 1 else None
|
||||
self.asset_id = parts[2] if len(parts) > 2 else None
|
||||
self.file_name = parts[3] if len(parts) > 2 else None
|
||||
self.collection = parts[1] if len(parts) > 1 else None
|
||||
self.collection_id = parts[2] if len(parts) > 2 else None
|
||||
self.asset_id = parts[3] if len(parts) > 3 else None
|
||||
self.file_name = parts[4] if len(parts) > 3 else None
|
||||
self.mime_type = parts[5] if len(parts) > 3 else None
|
||||
|
||||
|
||||
class ImmichMediaSource(MediaSource):
|
||||
@@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
|
||||
|
||||
name = "Immich"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize Immich media source."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.entries = entries
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
|
||||
raise BrowseError("Immich is not configured")
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
@@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
children=[
|
||||
*await self._async_build_immich(item),
|
||||
*await self._async_build_immich(item, entries),
|
||||
],
|
||||
)
|
||||
|
||||
async def _async_build_immich(
|
||||
self, item: MediaSourceItem
|
||||
self, item: MediaSourceItem, entries: list[ConfigEntry]
|
||||
) -> list[BrowseMediaSource]:
|
||||
"""Handle browsing different immich instances."""
|
||||
if not item.identifier:
|
||||
LOGGER.debug("Render all Immich instances")
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
@@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for entry in self.entries
|
||||
for entry in entries
|
||||
]
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
entry: ImmichConfigEntry | None = (
|
||||
@@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
|
||||
assert entry
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
if identifier.album_id is None:
|
||||
# Get Albums
|
||||
if identifier.collection is None:
|
||||
LOGGER.debug("Render all collections for %s", entry.title)
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{identifier.unique_id}|albums",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title="albums",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
]
|
||||
|
||||
if identifier.collection_id is None:
|
||||
LOGGER.debug("Render all albums for %s", entry.title)
|
||||
try:
|
||||
albums = await immich_api.albums.async_get_all_albums()
|
||||
except ImmichError:
|
||||
@@ -118,21 +130,25 @@ class ImmichMediaSource(MediaSource):
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{item.identifier}/{album.album_id}",
|
||||
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title=album.name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
)
|
||||
for album in albums
|
||||
]
|
||||
|
||||
# Request items of album
|
||||
LOGGER.debug(
|
||||
"Render all assets of album %s for %s",
|
||||
identifier.collection_id,
|
||||
entry.title,
|
||||
)
|
||||
try:
|
||||
album_info = await immich_api.albums.async_get_album_info(
|
||||
identifier.album_id
|
||||
identifier.collection_id
|
||||
)
|
||||
except ImmichError:
|
||||
return []
|
||||
@@ -141,17 +157,18 @@ class ImmichMediaSource(MediaSource):
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.file_name}|"
|
||||
f"{asset.mime_type}"
|
||||
),
|
||||
media_class=MediaClass.IMAGE,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=False,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("image/")
|
||||
@@ -161,17 +178,18 @@ class ImmichMediaSource(MediaSource):
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.file_name}|"
|
||||
f"{asset.mime_type}"
|
||||
),
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("video/")
|
||||
@@ -181,17 +199,23 @@ class ImmichMediaSource(MediaSource):
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
if identifier.file_name is None:
|
||||
raise Unresolvable("No file name")
|
||||
mime_type, _ = mimetypes.guess_type(identifier.file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise Unresolvable("No file extension")
|
||||
try:
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
except IndexError as err:
|
||||
raise Unresolvable(
|
||||
f"Could not parse identifier: {item.identifier}"
|
||||
) from err
|
||||
|
||||
if identifier.mime_type is None:
|
||||
raise Unresolvable(
|
||||
f"Could not resolve identifier that has no mime-type: {item.identifier}"
|
||||
)
|
||||
|
||||
return PlayMedia(
|
||||
(
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
|
||||
),
|
||||
mime_type,
|
||||
identifier.mime_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -212,10 +236,10 @@ class ImmichMediaView(HomeAssistantView):
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
raise HTTPNotFound
|
||||
|
||||
asset_id, file_name, size = location.split("/")
|
||||
mime_type, _ = mimetypes.guess_type(file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise HTTPNotFound
|
||||
try:
|
||||
asset_id, size, mime_type_base, mime_type_format = location.split("/")
|
||||
except ValueError as err:
|
||||
raise HTTPNotFound from err
|
||||
|
||||
entry: ImmichConfigEntry | None = (
|
||||
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||
@@ -226,7 +250,7 @@ class ImmichMediaView(HomeAssistantView):
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
# stream response for videos
|
||||
if mime_type.startswith("video/"):
|
||||
if mime_type_base == "video":
|
||||
try:
|
||||
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
||||
except ImmichError as exc:
|
||||
@@ -243,4 +267,4 @@ class ImmichMediaView(HomeAssistantView):
|
||||
image = await immich_api.assets.async_view_asset(asset_id, size)
|
||||
except ImmichError as exc:
|
||||
raise HTTPNotFound from exc
|
||||
return Response(body=image, content_type=mime_type)
|
||||
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.15"]
|
||||
"requirements": ["pyiskra==0.1.19"]
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
|
||||
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
|
||||
coordinator = JellyfinDataUpdateCoordinator(
|
||||
hass, entry, client, server_info, user_id
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
entry.async_on_unload(client.stop)
|
||||
|
||||
|
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
|
||||
"""Initialize the Jellyfin entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -11,8 +11,9 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "keyboard"
|
||||
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Listen for keyboard events."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Keyboard",
|
||||
},
|
||||
)
|
||||
|
||||
keyboard = PyKeyboard()
|
||||
keyboard.special_key_assignment()
|
||||
|
@@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
||||
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.6"]
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
}
|
||||
|
@@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
key="prebrew_on",
|
||||
translation_key="prebrew_time_on",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
key="prebrew_off",
|
||||
translation_key="prebrew_time_off",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"name": "LG ThinQ",
|
||||
"codeowners": ["@LG-ThinQ-Integration"],
|
||||
"config_flow": true,
|
||||
"dhcp": [{ "macaddress": "34E6E6*" }],
|
||||
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["thinqconnect"],
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.8"],
|
||||
"requirements": ["python-linkplay==0.2.9"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
@@ -7,8 +7,9 @@ import time
|
||||
import lirc
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the LIRC capability."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "LIRC",
|
||||
},
|
||||
)
|
||||
# blocking=True gives unexpected behavior (multiple responses for 1 press)
|
||||
# also by not blocking, we allow hass to shut down the thread gracefully
|
||||
# on exit.
|
||||
|
@@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
|
||||
# don't discover this entry if the supported state list is empty
|
||||
secondary_value_is_not=[],
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="MinPINCodeLength",
|
||||
translation_key="min_pin_code_length",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=None,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="MaxPINCodeLength",
|
||||
translation_key="max_pin_code_length",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=None,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="TargetPositionLiftPercent100ths",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key="window_covering_target_position",
|
||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
|
@@ -390,12 +390,6 @@
|
||||
"evse_user_max_charge_current": {
|
||||
"name": "User max charge current"
|
||||
},
|
||||
"min_pin_code_length": {
|
||||
"name": "Min PIN code length"
|
||||
},
|
||||
"max_pin_code_length": {
|
||||
"name": "Max PIN code length"
|
||||
},
|
||||
"window_covering_target_position": {
|
||||
"name": "Target opening position"
|
||||
}
|
||||
|
@@ -63,16 +63,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_invalid_auth": {
|
||||
"title": "The MELCloud YAML configuration import failed",
|
||||
"description": "Configuring MELCloud using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The MELCloud YAML configuration import failed",
|
||||
"description": "Configuring MELCloud using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to MELCloud works and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"room_temperature": {
|
||||
|
@@ -39,6 +39,7 @@ from homeassistant.components.light import (
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASS_UNITS,
|
||||
STATE_CLASS_UNITS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
@@ -640,6 +641,13 @@ def validate_sensor_platform_config(
|
||||
):
|
||||
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
|
||||
|
||||
if (
|
||||
(state_class := config.get(CONF_STATE_CLASS)) is not None
|
||||
and state_class in STATE_CLASS_UNITS
|
||||
and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class]
|
||||
):
|
||||
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class"
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -676,11 +684,19 @@ class PlatformField:
|
||||
@callback
|
||||
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
|
||||
"""Return a context based unit of measurement selector."""
|
||||
|
||||
if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS:
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]],
|
||||
sort=True,
|
||||
custom_value=True,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
user_data is None
|
||||
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
|
||||
or device_class not in DEVICE_CLASS_UNITS
|
||||
):
|
||||
device_class := user_data.get(CONF_DEVICE_CLASS)
|
||||
) is None or device_class not in DEVICE_CLASS_UNITS:
|
||||
return TEXT_SELECTOR
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
|
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_UNITS,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
ENTITY_ID_FORMAT,
|
||||
STATE_CLASS_UNITS,
|
||||
STATE_CLASSES_SCHEMA,
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
@@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
|
||||
)
|
||||
|
||||
if (
|
||||
(state_class := config.get(CONF_STATE_CLASS)) is not None
|
||||
and state_class in STATE_CLASS_UNITS
|
||||
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
not in STATE_CLASS_UNITS[state_class]
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"The unit of measurement '{unit_of_measurement}' is not valid "
|
||||
f"together with state class '{state_class}'"
|
||||
)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
|
||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
) is None:
|
||||
|
@@ -644,6 +644,7 @@
|
||||
"invalid_template": "Invalid template",
|
||||
"invalid_supported_color_modes": "Invalid supported color modes selection",
|
||||
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
||||
"invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
||||
"invalid_url": "Invalid URL",
|
||||
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
|
||||
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",
|
||||
|
@@ -58,15 +58,3 @@ class NikoHomeControlConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import a config entry."""
|
||||
self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]})
|
||||
error = await test_connection(import_info[CONF_HOST])
|
||||
|
||||
if not error:
|
||||
return self.async_create_entry(
|
||||
title="Niko Home Control",
|
||||
data={CONF_HOST: import_info[CONF_HOST]},
|
||||
)
|
||||
return self.async_abort(reason=error)
|
||||
|
@@ -5,80 +5,19 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from nhc.light import NHCLight
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
brightness_supported,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import NHCController, NikoHomeControlConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import NikoHomeControlEntity
|
||||
|
||||
# delete after 2025.7.0
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Niko Home Control light platform."""
|
||||
# Start import flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
if (
|
||||
result.get("type") == FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Niko Home Control",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Niko Home Control",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@@ -17,11 +17,5 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "YAML import failed due to a connection error",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ SUPPORT_FLAGS = (
|
||||
PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
|
||||
|
||||
MIN_TEMPERATURE = 7
|
||||
MAX_TEMPERATURE = 40
|
||||
MAX_TEMPERATURE = 30
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import NukiEntryData
|
||||
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN as NUKI_DOMAIN, ERROR_STATES
|
||||
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN, ERROR_STATES
|
||||
from .entity import NukiEntity
|
||||
from .helpers import CannotConnect
|
||||
|
||||
@@ -29,7 +29,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Nuki lock platform."""
|
||||
entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id]
|
||||
entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry_data.coordinator
|
||||
|
||||
entities: list[NukiDeviceEntity] = [
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
|
||||
from .entity import OneWireEntity, OneWireEntityDescription
|
||||
from .onewirehub import (
|
||||
SIGNAL_NEW_DEVICE_CONNECTED,
|
||||
@@ -37,13 +37,14 @@ class OneWireBinarySensorEntityDescription(
|
||||
):
|
||||
"""Class describing OneWire binary sensor entities."""
|
||||
|
||||
read_mode = READ_MODE_INT
|
||||
|
||||
|
||||
DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
|
||||
"12": tuple(
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"sensed.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="sensed_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -53,7 +54,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"sensed.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="sensed_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -63,7 +63,6 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"sensed.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="sensed_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -78,7 +77,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"hub/short.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
translation_key="hub_short_id",
|
||||
@@ -162,4 +160,4 @@ class OneWireBinarySensorEntity(OneWireEntity, BinarySensorEntity):
|
||||
"""Return true if sensor is on."""
|
||||
if self._state is None:
|
||||
return None
|
||||
return bool(self._state)
|
||||
return self._state == 1
|
||||
|
@@ -51,6 +51,5 @@ MANUFACTURER_MAXIM = "Maxim Integrated"
|
||||
MANUFACTURER_HOBBYBOARDS = "Hobby Boards"
|
||||
MANUFACTURER_EDS = "Embedded Data Systems"
|
||||
|
||||
READ_MODE_BOOL = "bool"
|
||||
READ_MODE_FLOAT = "float"
|
||||
READ_MODE_INT = "int"
|
||||
|
@@ -10,9 +10,8 @@ from pyownet import protocol
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import READ_MODE_BOOL, READ_MODE_INT
|
||||
from .const import READ_MODE_INT
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -45,7 +44,7 @@ class OneWireEntity(Entity):
|
||||
self._attr_unique_id = f"/{device_id}/{description.key}"
|
||||
self._attr_device_info = device_info
|
||||
self._device_file = device_file
|
||||
self._state: StateType = None
|
||||
self._state: int | float | None = None
|
||||
self._value_raw: float | None = None
|
||||
self._owproxy = owproxy
|
||||
|
||||
@@ -82,7 +81,5 @@ class OneWireEntity(Entity):
|
||||
_LOGGER.debug("Fetching %s data recovered", self.name)
|
||||
if self.entity_description.read_mode == READ_MODE_INT:
|
||||
self._state = int(self._value_raw)
|
||||
elif self.entity_description.read_mode == READ_MODE_BOOL:
|
||||
self._state = int(self._value_raw) == 1
|
||||
else:
|
||||
self._state = self._value_raw
|
||||
|
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
|
||||
from .entity import OneWireEntity, OneWireEntityDescription
|
||||
from .onewirehub import (
|
||||
SIGNAL_NEW_DEVICE_CONNECTED,
|
||||
@@ -32,13 +32,14 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
class OneWireSwitchEntityDescription(OneWireEntityDescription, SwitchEntityDescription):
|
||||
"""Class describing OneWire switch entities."""
|
||||
|
||||
read_mode = READ_MODE_INT
|
||||
|
||||
|
||||
DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
"05": (
|
||||
OneWireSwitchEntityDescription(
|
||||
key="PIO",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio",
|
||||
),
|
||||
),
|
||||
@@ -47,7 +48,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"PIO.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -57,7 +57,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"latch.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="latch_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -69,7 +68,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
key="IAD",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="iad",
|
||||
),
|
||||
),
|
||||
@@ -78,7 +76,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"PIO.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -88,7 +85,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"latch.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="latch_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -99,7 +95,6 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"PIO.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -115,7 +110,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"hub/branch.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="hub_branch_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
@@ -127,7 +121,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"moisture/is_leaf.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="leaf_sensor_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
@@ -138,7 +131,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"moisture/is_moisture.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="moisture_sensor_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
@@ -226,7 +218,7 @@ class OneWireSwitchEntity(OneWireEntity, SwitchEntity):
|
||||
"""Return true if switch is on."""
|
||||
if self._state is None:
|
||||
return None
|
||||
return bool(self._state)
|
||||
return self._state == 1
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.12.2"]
|
||||
"requirements": ["opower==0.12.3"]
|
||||
}
|
||||
|
@@ -1 +1,3 @@
|
||||
"""The pandora component."""
|
||||
|
||||
DOMAIN = "pandora"
|
||||
|
@@ -27,10 +27,13 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_DOWN,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -53,6 +56,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Pandora media player platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Pandora",
|
||||
},
|
||||
)
|
||||
|
||||
if not _pianobar_exists():
|
||||
return
|
||||
pandora = PandoraMediaPlayer("Pandora")
|
||||
|
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyprobeplus==1.0.0"]
|
||||
"requirements": ["pyprobeplus==1.0.1"]
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.40",
|
||||
"SQLAlchemy==2.0.41",
|
||||
"fnv-hash-fast==1.5.0",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
|
@@ -233,6 +233,14 @@ async def async_setup_entry(
|
||||
"privacy_mode_change", async_privacy_mode_change, 623
|
||||
)
|
||||
|
||||
# ensure host device is setup before connected camera devices that use via_device
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, host.unique_id)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, host.api.mac_address)},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
|
@@ -462,6 +462,12 @@
|
||||
"doorbell_button_sound": {
|
||||
"default": "mdi:volume-high"
|
||||
},
|
||||
"hardwired_chime_enabled": {
|
||||
"default": "mdi:bell",
|
||||
"state": {
|
||||
"off": "mdi:bell-off"
|
||||
}
|
||||
},
|
||||
"hdr": {
|
||||
"default": "mdi:hdr"
|
||||
},
|
||||
|
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.13.3"]
|
||||
"requirements": ["reolink-aio==0.13.4"]
|
||||
}
|
||||
|
@@ -910,6 +910,9 @@
|
||||
"auto_focus": {
|
||||
"name": "Auto focus"
|
||||
},
|
||||
"hardwired_chime_enabled": {
|
||||
"name": "Hardwired chime enabled"
|
||||
},
|
||||
"guard_return": {
|
||||
"name": "Guard return"
|
||||
},
|
||||
|
@@ -216,6 +216,16 @@ SWITCH_ENTITIES = (
|
||||
value=lambda api, ch: api.baichuan.privacy_mode(ch),
|
||||
method=lambda api, ch, value: api.baichuan.set_privacy_mode(ch, value),
|
||||
),
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="hardwired_chime_enabled",
|
||||
cmd_key="483",
|
||||
translation_key="hardwired_chime_enabled",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
supported=lambda api, ch: api.supported(ch, "hardwired_chime"),
|
||||
value=lambda api, ch: api.baichuan.hardwired_chime_enabled(ch),
|
||||
method=lambda api, ch, value: api.baichuan.set_ding_dong_ctrl(ch, enable=value),
|
||||
),
|
||||
)
|
||||
|
||||
NVR_SWITCH_ENTITIES = (
|
||||
|
@@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
verify_ssl=False,
|
||||
ssl_cipher=SSLCipherList.INSECURE,
|
||||
)
|
||||
self._vod_type: str | None = None
|
||||
|
||||
async def get(
|
||||
self,
|
||||
@@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
|
||||
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
|
||||
ch = int(channel)
|
||||
if self._vod_type is not None:
|
||||
vod_type = self._vod_type
|
||||
try:
|
||||
host = get_host(self.hass, config_entry_id)
|
||||
except Unresolvable:
|
||||
@@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
"apolication/octet-stream",
|
||||
]:
|
||||
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
|
||||
if (
|
||||
reolink_response.content_type == "video/x-flv"
|
||||
and vod_type == VodRequestType.PLAYBACK.value
|
||||
):
|
||||
# next time use DOWNLOAD immediately
|
||||
self._vod_type = VodRequestType.DOWNLOAD.value
|
||||
_LOGGER.debug(
|
||||
"%s, retrying using download instead of playback cmd", err_str
|
||||
)
|
||||
return await self.get(
|
||||
request,
|
||||
config_entry_id,
|
||||
channel,
|
||||
stream_res,
|
||||
self._vod_type,
|
||||
filename,
|
||||
retry,
|
||||
)
|
||||
|
||||
_LOGGER.error(err_str)
|
||||
if reolink_response.content_type == "text/html":
|
||||
text = await reolink_response.text()
|
||||
@@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
reolink_response.reason,
|
||||
response_headers,
|
||||
)
|
||||
response_headers["Content-Type"] = "video/mp4"
|
||||
if "Content-Type" not in response_headers:
|
||||
response_headers["Content-Type"] = reolink_response.content_type
|
||||
if response_headers["Content-Type"] == "apolication/octet-stream":
|
||||
response_headers["Content-Type"] = "application/octet-stream"
|
||||
|
||||
response = web.StreamResponse(
|
||||
status=reolink_response.status,
|
||||
|
@@ -92,13 +92,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
"""Mark the first item with matching `name` as completed."""
|
||||
data = hass.data[DOMAIN]
|
||||
name = call.data[ATTR_NAME]
|
||||
|
||||
try:
|
||||
item = [item for item in data.items if item["name"] == name][0]
|
||||
except IndexError:
|
||||
_LOGGER.error("Updating of item failed: %s cannot be found", name)
|
||||
else:
|
||||
await data.async_update(item["id"], {"name": name, "complete": True})
|
||||
await data.async_complete(name)
|
||||
except NoMatchingShoppingListItem:
|
||||
_LOGGER.error("Completing of item failed: %s cannot be found", name)
|
||||
|
||||
async def incomplete_item_service(call: ServiceCall) -> None:
|
||||
"""Mark the first item with matching `name` as incomplete."""
|
||||
@@ -258,6 +255,30 @@ class ShoppingData:
|
||||
)
|
||||
return removed
|
||||
|
||||
async def async_complete(
|
||||
self, name: str, context: Context | None = None
|
||||
) -> list[dict[str, JsonValueType]]:
|
||||
"""Mark all shopping list items with the given name as complete."""
|
||||
complete_items = [
|
||||
item for item in self.items if item["name"] == name and not item["complete"]
|
||||
]
|
||||
|
||||
if len(complete_items) == 0:
|
||||
raise NoMatchingShoppingListItem
|
||||
|
||||
for item in complete_items:
|
||||
_LOGGER.debug("Completing %s", item)
|
||||
item["complete"] = True
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
for item in complete_items:
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
{"action": "complete", "item": item},
|
||||
context=context,
|
||||
)
|
||||
return complete_items
|
||||
|
||||
async def async_update(
|
||||
self, item_id: str | None, info: dict[str, Any], context: Context | None = None
|
||||
) -> dict[str, JsonValueType]:
|
||||
|
@@ -5,15 +5,17 @@ from __future__ import annotations
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, intent
|
||||
|
||||
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED
|
||||
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED, NoMatchingShoppingListItem
|
||||
|
||||
INTENT_ADD_ITEM = "HassShoppingListAddItem"
|
||||
INTENT_COMPLETE_ITEM = "HassShoppingListCompleteItem"
|
||||
INTENT_LAST_ITEMS = "HassShoppingListLastItems"
|
||||
|
||||
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""Set up the Shopping List intents."""
|
||||
intent.async_register(hass, AddItemIntent())
|
||||
intent.async_register(hass, CompleteItemIntent())
|
||||
intent.async_register(hass, ListTopItemsIntent())
|
||||
|
||||
|
||||
@@ -36,6 +38,33 @@ class AddItemIntent(intent.IntentHandler):
|
||||
return response
|
||||
|
||||
|
||||
class CompleteItemIntent(intent.IntentHandler):
|
||||
"""Handle CompleteItem intents."""
|
||||
|
||||
intent_type = INTENT_COMPLETE_ITEM
|
||||
description = "Marks an item as completed on the shopping list"
|
||||
slot_schema = {"item": cv.string}
|
||||
platforms = {DOMAIN}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
item = slots["item"]["value"].strip()
|
||||
|
||||
try:
|
||||
complete_items = await intent_obj.hass.data[DOMAIN].async_complete(item)
|
||||
except NoMatchingShoppingListItem:
|
||||
complete_items = []
|
||||
|
||||
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech_slots({"completed_items": complete_items})
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ListTopItemsIntent(intent.IntentHandler):
|
||||
"""Handle AddItem intents."""
|
||||
|
||||
@@ -47,7 +76,7 @@ class ListTopItemsIntent(intent.IntentHandler):
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
items = intent_obj.hass.data[DOMAIN].items[-5:]
|
||||
response = intent_obj.create_response()
|
||||
response: intent.IntentResponse = intent_obj.create_response()
|
||||
|
||||
if not items:
|
||||
response.async_set_speech("There are no items on your shopping list")
|
||||
|
@@ -1,25 +1,37 @@
|
||||
"""Common base for entities."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pysmarlaapi import Federwiege
|
||||
from pysmarlaapi.federwiege.classes import Property
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SmarlaEntityDescription(EntityDescription):
|
||||
"""Class describing Swing2Sleep Smarla entities."""
|
||||
|
||||
service: str
|
||||
property: str
|
||||
|
||||
|
||||
class SmarlaBaseEntity(Entity):
|
||||
"""Common Base Entity class for defining Smarla device."""
|
||||
|
||||
entity_description: SmarlaEntityDescription
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, federwiege: Federwiege, prop: Property) -> None:
|
||||
def __init__(self, federwiege: Federwiege, desc: SmarlaEntityDescription) -> None:
|
||||
"""Initialise the entity."""
|
||||
self._property = prop
|
||||
self.entity_description = desc
|
||||
self._property = federwiege.get_property(desc.service, desc.property)
|
||||
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, federwiege.serial_number)},
|
||||
name=DEVICE_MODEL_NAME,
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pysmarlaapi import Federwiege
|
||||
from pysmarlaapi.federwiege.classes import Property
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
@@ -11,16 +10,13 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import FederwiegeConfigEntry
|
||||
from .entity import SmarlaBaseEntity
|
||||
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
|
||||
class SmarlaSwitchEntityDescription(SmarlaEntityDescription, SwitchEntityDescription):
|
||||
"""Class describing Swing2Sleep Smarla switch entity."""
|
||||
|
||||
service: str
|
||||
property: str
|
||||
|
||||
|
||||
SWITCHES: list[SmarlaSwitchEntityDescription] = [
|
||||
SmarlaSwitchEntityDescription(
|
||||
@@ -55,17 +51,6 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
|
||||
|
||||
_property: Property[bool]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
federwiege: Federwiege,
|
||||
desc: SmarlaSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Smarla switch."""
|
||||
prop = federwiege.get_property(desc.service, desc.property)
|
||||
super().__init__(federwiege, prop)
|
||||
self.entity_description = desc
|
||||
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.4"],
|
||||
"requirements": ["pysmlight==0.2.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
@@ -6,9 +6,14 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -52,6 +58,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Configure Gammu state machine."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
DEPRECATED_ISSUE_ID,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_config_flow_integration",
|
||||
translation_placeholders={
|
||||
"integration_title": "SMS notifications via GSM-modem",
|
||||
},
|
||||
)
|
||||
|
||||
device = entry.data[CONF_DEVICE]
|
||||
connection_mode = "at"
|
||||
@@ -101,4 +120,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
|
||||
await gateway.terminate_async()
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID)
|
||||
|
||||
return unload_ok
|
||||
|
@@ -7,8 +7,13 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, intent
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "snips"
|
||||
@@ -91,6 +96,20 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate Snips component."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Snips",
|
||||
},
|
||||
)
|
||||
|
||||
# Make sure MQTT integration is enabled and the client is available
|
||||
if not await mqtt.async_wait_for_mqtt_client(hass):
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sql",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["SQLAlchemy==2.0.40", "sqlparse==0.5.0"]
|
||||
"requirements": ["SQLAlchemy==2.0.41", "sqlparse==0.5.0"]
|
||||
}
|
||||
|
@@ -26,14 +26,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN as SWITCH_DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
DEFAULT_NAME = "Light Switch"
|
||||
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN),
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ class LightSwitch(LightEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Forward the turn_on command to the switch in this light switch."""
|
||||
await self.hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: self._switch_entity_id},
|
||||
blocking=True,
|
||||
@@ -86,7 +86,7 @@ class LightSwitch(LightEntity):
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Forward the turn_off command to the switch in this light switch."""
|
||||
await self.hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: self._switch_entity_id},
|
||||
blocking=True,
|
||||
|
@@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, ToggleEntity
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from .const import DOMAIN as SWITCH_AS_X_DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class BaseEntity(Entity):
|
||||
@@ -61,7 +61,7 @@ class BaseEntity(Entity):
|
||||
self._switch_entity_id = switch_entity_id
|
||||
|
||||
self._is_new_entity = (
|
||||
registry.async_get_entity_id(domain, SWITCH_AS_X_DOMAIN, unique_id) is None
|
||||
registry.async_get_entity_id(domain, DOMAIN, unique_id) is None
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -102,7 +102,7 @@ class BaseEntity(Entity):
|
||||
if registry.async_get(self.entity_id) is not None:
|
||||
registry.async_update_entity_options(
|
||||
self.entity_id,
|
||||
SWITCH_AS_X_DOMAIN,
|
||||
DOMAIN,
|
||||
self.async_generate_entity_options(),
|
||||
)
|
||||
|
||||
|
@@ -7,7 +7,13 @@ from dataclasses import dataclass, field
|
||||
from logging import getLogger
|
||||
|
||||
from aiohttp import web
|
||||
from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI
|
||||
from switchbot_api import (
|
||||
Device,
|
||||
Remote,
|
||||
SwitchBotAPI,
|
||||
SwitchBotAuthenticationError,
|
||||
SwitchBotConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -175,12 +181,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
api = SwitchBotAPI(token=token, secret=secret)
|
||||
try:
|
||||
devices = await api.list_devices()
|
||||
except InvalidAuth as ex:
|
||||
except SwitchBotAuthenticationError as ex:
|
||||
_LOGGER.error(
|
||||
"Invalid authentication while connecting to SwitchBot API: %s", ex
|
||||
)
|
||||
return False
|
||||
except CannotConnect as ex:
|
||||
except SwitchBotConnectionError as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
_LOGGER.debug("Devices: %s", devices)
|
||||
coordinators_by_id: dict[str, SwitchBotCoordinator] = {}
|
||||
|
@@ -3,7 +3,11 @@
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI
|
||||
from switchbot_api import (
|
||||
SwitchBotAPI,
|
||||
SwitchBotAuthenticationError,
|
||||
SwitchBotConnectionError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -36,9 +40,9 @@ class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await SwitchBotAPI(
|
||||
token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY]
|
||||
).list_devices()
|
||||
except CannotConnect:
|
||||
except SwitchBotConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
except SwitchBotAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
@@ -4,7 +4,7 @@ from asyncio import timeout
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI
|
||||
from switchbot_api import Device, Remote, SwitchBotAPI, SwitchBotConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -70,5 +70,5 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]):
|
||||
status: Status = await self._api.get_status(self._device_id)
|
||||
_LOGGER.debug("Refreshing %s with %s", self._device_id, status)
|
||||
return status
|
||||
except CannotConnect as err:
|
||||
except SwitchBotConnectionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.3.1"]
|
||||
"requirements": ["switchbot-api==2.4.0"]
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiotedee"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotedee==0.2.20"]
|
||||
"requirements": ["aiotedee==0.2.23"]
|
||||
}
|
||||
|
@@ -1 +1,4 @@
|
||||
"""The tensorflow component."""
|
||||
|
||||
DOMAIN = "tensorflow"
|
||||
CONF_GRAPH = "graph"
|
||||
|
@@ -26,15 +26,21 @@ from homeassistant.const import (
|
||||
CONF_SOURCE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.pil import draw_box
|
||||
|
||||
from . import CONF_GRAPH, DOMAIN
|
||||
|
||||
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
||||
|
||||
DOMAIN = "tensorflow"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MATCHES = "matches"
|
||||
@@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom"
|
||||
CONF_CATEGORIES = "categories"
|
||||
CONF_CATEGORY = "category"
|
||||
CONF_FILE_OUT = "file_out"
|
||||
CONF_GRAPH = "graph"
|
||||
CONF_LABELS = "labels"
|
||||
CONF_LABEL_OFFSET = "label_offset"
|
||||
CONF_LEFT = "left"
|
||||
@@ -110,6 +115,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the TensorFlow image processing platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Tensorflow",
|
||||
},
|
||||
)
|
||||
|
||||
model_config = config[CONF_MODEL]
|
||||
model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow")
|
||||
labels = model_config.get(CONF_LABELS) or hass.config.path(
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.17"]
|
||||
"requirements": ["tesla-fleet-api==1.1.1"]
|
||||
}
|
||||
|
@@ -125,6 +125,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
key="charge_state_conn_charge_cable",
|
||||
polling=True,
|
||||
polling_value_fn=lambda x: x != "<invalid>",
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType(
|
||||
lambda value: callback(value != "Unknown")
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.9"]
|
||||
"requirements": ["tesla-fleet-api==1.1.1", "teslemetry-stream==0.7.9"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.17"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.1.1"]
|
||||
}
|
||||
|
@@ -20,6 +20,10 @@ STATES = {
|
||||
"Stopped": MediaPlayerState.IDLE,
|
||||
}
|
||||
|
||||
# Tesla uses 31 steps, in 0.333 increments up to 10.333
|
||||
VOLUME_STEP = 1 / 31
|
||||
VOLUME_FACTOR = 31 / 3 # 10.333
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
|
||||
"""Vehicle Location Media Class."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
_attr_volume_step = VOLUME_STEP
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def volume_level(self) -> float:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self.get("vehicle_state_media_info_audio_volume", 0) / self.get(
|
||||
"vehicle_state_media_info_audio_volume_max", 10.333333
|
||||
)
|
||||
return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR
|
||||
|
||||
@property
|
||||
def media_duration(self) -> int | None:
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.helpers.device_registry import (
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN, PLATFORMS
|
||||
from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN, PLATFORMS
|
||||
from .config import UnifiConfig
|
||||
from .entity_helper import UnifiEntityHelper
|
||||
from .entity_loader import UnifiEntityLoader
|
||||
@@ -104,7 +104,7 @@ class UnifiHub:
|
||||
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(UNIFI_DOMAIN, self.config.entry.unique_id)},
|
||||
identifiers={(DOMAIN, self.config.entry.unique_id)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model="UniFi Network Application",
|
||||
name="UniFi Network",
|
||||
|
@@ -52,7 +52,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import UnifiConfigEntry
|
||||
from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
|
||||
from .const import ATTR_MANUFACTURER, DOMAIN
|
||||
from .entity import (
|
||||
HandlerT,
|
||||
SubscriptionT,
|
||||
@@ -367,14 +367,12 @@ def async_update_unique_id(hass: HomeAssistant, config_entry: UnifiConfigEntry)
|
||||
def update_unique_id(obj_id: str, type_name: str) -> None:
|
||||
"""Rework unique ID."""
|
||||
new_unique_id = f"{type_name}-{obj_id}"
|
||||
if ent_reg.async_get_entity_id(SWITCH_DOMAIN, UNIFI_DOMAIN, new_unique_id):
|
||||
if ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, new_unique_id):
|
||||
return
|
||||
|
||||
prefix, _, suffix = obj_id.partition("_")
|
||||
unique_id = f"{prefix}-{type_name}-{suffix}"
|
||||
if entity_id := ent_reg.async_get_entity_id(
|
||||
SWITCH_DOMAIN, UNIFI_DOMAIN, unique_id
|
||||
):
|
||||
if entity_id := ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id):
|
||||
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
for obj_id in hub.api.outlets:
|
||||
|
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@@ -300,7 +300,9 @@ async def handle_call_service(
|
||||
translation_placeholders=err.translation_placeholders,
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
connection.logger.exception("Unexpected exception")
|
||||
connection.logger.error(
|
||||
"Error during service call to %s.%s: %s", msg["domain"], msg["service"], err
|
||||
)
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
const.ERR_HOME_ASSISTANT_ERROR,
|
||||
|
@@ -94,21 +94,59 @@ def _get_obj_holidays(
|
||||
language=language,
|
||||
categories=set_categories,
|
||||
)
|
||||
|
||||
supported_languages = obj_holidays.supported_languages
|
||||
default_language = obj_holidays.default_language
|
||||
|
||||
if default_language and not language:
|
||||
# If no language is set, use the default language
|
||||
LOGGER.debug("Changing language from None to %s", default_language)
|
||||
return country_holidays( # Return default if no language
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=default_language,
|
||||
categories=set_categories,
|
||||
)
|
||||
|
||||
if (
|
||||
(supported_languages := obj_holidays.supported_languages)
|
||||
default_language
|
||||
and language
|
||||
and language not in supported_languages
|
||||
and language.startswith("en")
|
||||
):
|
||||
# If language does not match supported languages, use the first English variant
|
||||
if default_language.startswith("en"):
|
||||
LOGGER.debug("Changing language from %s to %s", language, default_language)
|
||||
return country_holidays( # Return default English if default language
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=default_language,
|
||||
categories=set_categories,
|
||||
)
|
||||
for lang in supported_languages:
|
||||
if lang.startswith("en"):
|
||||
obj_holidays = country_holidays(
|
||||
LOGGER.debug("Changing language from %s to %s", language, lang)
|
||||
return country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=lang,
|
||||
categories=set_categories,
|
||||
)
|
||||
LOGGER.debug("Changing language from %s to %s", language, lang)
|
||||
|
||||
if default_language and language and language not in supported_languages:
|
||||
# If language does not match supported languages, use the default language
|
||||
LOGGER.debug("Changing language from %s to %s", language, default_language)
|
||||
return country_holidays( # Return default English if default language
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=default_language,
|
||||
categories=set_categories,
|
||||
)
|
||||
|
||||
return obj_holidays
|
||||
|
||||
|
||||
|
@@ -67,8 +67,7 @@ def add_province_and_language_to_schema(
|
||||
|
||||
_country = country_holidays(country=country)
|
||||
if country_default_language := (_country.default_language):
|
||||
selectable_languages = _country.supported_languages
|
||||
new_selectable_languages = list(selectable_languages)
|
||||
new_selectable_languages = list(_country.supported_languages)
|
||||
language_schema = {
|
||||
vol.Optional(
|
||||
CONF_LANGUAGE, default=country_default_language
|
||||
@@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None:
|
||||
years=year,
|
||||
language=language,
|
||||
)
|
||||
if (
|
||||
(supported_languages := obj_holidays.supported_languages)
|
||||
and language
|
||||
and language.startswith("en")
|
||||
):
|
||||
for lang in supported_languages:
|
||||
if lang.startswith("en"):
|
||||
obj_holidays = country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=lang,
|
||||
)
|
||||
|
||||
else:
|
||||
obj_holidays = HolidayBase(years=year)
|
||||
|
||||
|
@@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, api.mac)},
|
||||
manufacturer=api.brand,
|
||||
name=f"{api.network_name}",
|
||||
name=api.network_name,
|
||||
model="Zimi Cloud Connect",
|
||||
sw_version=api.firmware_version,
|
||||
connections={(CONNECTION_NETWORK_MAC, api.mac)},
|
||||
|
@@ -32,7 +32,7 @@ async def async_setup_entry(
|
||||
]
|
||||
|
||||
lights.extend(
|
||||
[ZimiDimmer(device, api) for device in api.lights if device.type == "dimmer"]
|
||||
ZimiDimmer(device, api) for device in api.lights if device.type == "dimmer"
|
||||
)
|
||||
|
||||
async_add_entities(lights)
|
||||
@@ -81,8 +81,6 @@ class ZimiDimmer(ZimiLight):
|
||||
super().__init__(device, api)
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
if self._device.type != "dimmer":
|
||||
raise ValueError("ZimiDimmer needs a dimmable light")
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on (with optional brightness)."""
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user