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