Merge branch 'dev' into core_trigger_descriptions

This commit is contained in:
Erik Montnemery
2025-06-24 10:24:48 +02:00
committed by GitHub
253 changed files with 20460 additions and 1903 deletions

View File

@ -67,6 +67,7 @@ homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.alexa_devices.*
homeassistant.components.alpha_vantage.*
homeassistant.components.altruist.*
homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.*
@ -502,6 +503,7 @@ homeassistant.components.tautulli.*
homeassistant.components.tcp.*
homeassistant.components.technove.*
homeassistant.components.tedee.*
homeassistant.components.telegram_bot.*
homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*
homeassistant.components.threshold.*

8
CODEOWNERS generated
View File

@ -93,6 +93,8 @@ build.json @home-assistant/supervisor
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/alexa_devices/ @chemelli74
/tests/components/alexa_devices/ @chemelli74
/homeassistant/components/altruist/ @airalab @LoSk-p
/tests/components/altruist/ @airalab @LoSk-p
/homeassistant/components/amazon_polly/ @jschlyter
/homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot
@ -786,8 +788,6 @@ build.json @home-assistant/supervisor
/tests/components/jellyfin/ @RunC0deRun @ctalkington
/homeassistant/components/jewish_calendar/ @tsvi
/tests/components/jewish_calendar/ @tsvi
/homeassistant/components/juicenet/ @jesserockz
/tests/components/juicenet/ @jesserockz
/homeassistant/components/justnimbus/ @kvanzuijlen
/tests/components/justnimbus/ @kvanzuijlen
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
@ -1169,6 +1169,8 @@ build.json @home-assistant/supervisor
/tests/components/ping/ @jpbede
/homeassistant/components/plaato/ @JohNan
/tests/components/plaato/ @JohNan
/homeassistant/components/playstation_network/ @jackjpowell
/tests/components/playstation_network/ @jackjpowell
/homeassistant/components/plex/ @jjlawren
/tests/components/plex/ @jjlawren
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
@ -1670,6 +1672,8 @@ build.json @home-assistant/supervisor
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
/homeassistant/components/valve/ @home-assistant/core
/tests/components/valve/ @home-assistant/core
/homeassistant/components/vegehub/ @ghowevege
/tests/components/vegehub/ @ghowevege
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio

View File

@ -1,5 +1,11 @@
{
"domain": "sony",
"name": "Sony",
"integrations": ["braviatv", "ps4", "sony_projector", "songpal"]
"integrations": [
"braviatv",
"ps4",
"sony_projector",
"songpal",
"playstation_network"
]
}

View File

@ -5,6 +5,7 @@ import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import (
HassJobType,
HomeAssistant,
@ -17,9 +18,17 @@ from homeassistant.helpers import config_validation as cv, storage
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature
from .const import (
ATTR_INSTRUCTIONS,
ATTR_TASK_NAME,
DATA_COMPONENT,
DATA_PREFERENCES,
DOMAIN,
SERVICE_GENERATE_TEXT,
AITaskEntityFeature,
)
from .entity import AITaskEntity
from .http import async_setup as async_setup_conversation_http
from .http import async_setup as async_setup_http
from .task import GenTextTask, GenTextTaskResult, async_generate_text
__all__ = [
@ -45,16 +54,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DATA_COMPONENT] = entity_component
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
await hass.data[DATA_PREFERENCES].async_load()
async_setup_conversation_http(hass)
async_setup_http(hass)
hass.services.async_register(
DOMAIN,
"generate_text",
SERVICE_GENERATE_TEXT,
async_service_generate_text,
schema=vol.Schema(
{
vol.Required("task_name"): cv.string,
vol.Optional("entity_id"): cv.entity_id,
vol.Required("instructions"): cv.string,
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
}
),
supports_response=SupportsResponse.ONLY,

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from enum import IntFlag
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Final
from homeassistant.util.hass_dict import HassKey
@ -17,6 +17,11 @@ DOMAIN = "ai_task"
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
SERVICE_GENERATE_TEXT = "generate_text"
ATTR_INSTRUCTIONS: Final = "instructions"
ATTR_TASK_NAME: Final = "task_name"
DEFAULT_SYSTEM_PROMPT = (
"You are a Home Assistant expert and help users with their tasks."
)

View File

@ -6,7 +6,7 @@ generate_text:
selector:
text:
instructions:
example: "Generate a funny notification that garage door was left open"
example: "Generate a funny notification that the garage door was left open"
required: true
selector:
text:

View File

@ -5,7 +5,7 @@
"description": "Use AI to run a task that generates text.",
"fields": {
"task_name": {
"name": "Task Name",
"name": "Task name",
"description": "Name of the task."
},
"instructions": {

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
@ -21,14 +22,16 @@ async def async_generate_text(
entity_id = hass.data[DATA_PREFERENCES].gen_text_entity_id
if entity_id is None:
raise ValueError("No entity_id provided and no preferred entity set")
raise HomeAssistantError("No entity_id provided and no preferred entity set")
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
if entity is None:
raise ValueError(f"AI Task entity {entity_id} not found")
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features:
raise ValueError(f"AI Task entity {entity_id} does not support generating text")
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support generating text"
)
return await entity.internal_async_generate_text(
GenTextTask(

View File

@ -0,0 +1,27 @@
"""The Altruist integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import AltruistConfigEntry, AltruistDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AltruistConfigEntry) -> bool:
"""Set up Altruist from a config entry."""
coordinator = AltruistDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AltruistConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,107 @@
"""Config flow for the Altruist integration."""
import logging
from typing import Any
from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import CONF_HOST, DOMAIN
_LOGGER = logging.getLogger(__name__)
class AltruistConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Altruist."""
device: AltruistDeviceModel
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
ip_address = ""
if user_input is not None:
ip_address = user_input[CONF_HOST]
try:
client = await AltruistClient.from_ip_address(
async_get_clientsession(self.hass), ip_address
)
except AltruistError:
errors["base"] = "no_device_found"
else:
self.device = client.device
await self.async_set_unique_id(
client.device_id, raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self.device.id,
data={
CONF_HOST: ip_address,
},
)
data_schema = self.add_suggested_values_to_schema(
vol.Schema({vol.Required(CONF_HOST): str}),
{CONF_HOST: ip_address},
)
return self.async_show_form(
step_id="user",
data_schema=data_schema,
errors=errors,
description_placeholders={
"ip_address": ip_address,
},
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
_LOGGER.debug("Zeroconf discovery: %s", discovery_info)
try:
client = await AltruistClient.from_ip_address(
async_get_clientsession(self.hass), str(discovery_info.ip_address)
)
except AltruistError:
return self.async_abort(reason="no_device_found")
self.device = client.device
_LOGGER.debug("Zeroconf device: %s", client.device)
await self.async_set_unique_id(client.device_id)
self._abort_if_unique_id_configured()
self.context.update(
{
"title_placeholders": {
"name": self.device.id,
}
}
)
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
return self.async_create_entry(
title=self.device.id,
data={
CONF_HOST: self.device.ip_address,
},
)
self._set_confirm_only()
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={
"model": self.device.id,
},
)

View File

@ -0,0 +1,5 @@
"""Constants for the Altruist integration."""
DOMAIN = "altruist"
CONF_HOST = "host"

View File

@ -0,0 +1,64 @@
"""Coordinator module for Altruist integration in Home Assistant.
This module defines the AltruistDataUpdateCoordinator class, which manages
data updates for Altruist sensors using the AltruistClient.
"""
from datetime import timedelta
import logging
from altruistclient import AltruistClient, AltruistError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_HOST
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=15)
type AltruistConfigEntry = ConfigEntry[AltruistDataUpdateCoordinator]
class AltruistDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
"""Coordinates data updates for Altruist sensors."""
client: AltruistClient
def __init__(
self,
hass: HomeAssistant,
config_entry: AltruistConfigEntry,
) -> None:
"""Initialize the data update coordinator for Altruist sensors."""
device_id = config_entry.unique_id
super().__init__(
hass,
logger=_LOGGER,
config_entry=config_entry,
name=f"Altruist {device_id}",
update_interval=UPDATE_INTERVAL,
)
self._ip_address = config_entry.data[CONF_HOST]
async def _async_setup(self) -> None:
try:
self.client = await AltruistClient.from_ip_address(
async_get_clientsession(self.hass), self._ip_address
)
await self.client.fetch_data()
except AltruistError as e:
raise ConfigEntryNotReady("Error in Altruist setup") from e
async def _async_update_data(self) -> dict[str, str]:
try:
fetched_data = await self.client.fetch_data()
except AltruistError as ex:
raise UpdateFailed(
f"The Altruist {self.client.device_id} is unavailable: {ex}"
) from ex
return {item["value_type"]: item["value"] for item in fetched_data}

View File

@ -0,0 +1,15 @@
{
"entity": {
"sensor": {
"pm_10": {
"default": "mdi:thought-bubble"
},
"pm_25": {
"default": "mdi:thought-bubble-outline"
},
"radiation": {
"default": "mdi:radioactive"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"domain": "altruist",
"name": "Altruist",
"codeowners": ["@airalab", "@LoSk-p"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/altruist",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["altruistclient==0.1.1"],
"zeroconf": ["_altruist._tcp.local."]
}

View File

@ -0,0 +1,83 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not provide options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
Device type integration
entity-category: todo
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: No known use cases for repair issues or flows, yet
stale-devices:
status: exempt
comment: |
Device type integration
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@ -0,0 +1,249 @@
"""Defines the Altruist sensor platform."""
from collections.abc import Callable
from dataclasses import dataclass
import logging
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AltruistConfigEntry
from .coordinator import AltruistDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True)
class AltruistSensorEntityDescription(SensorEntityDescription):
"""Class to describe a Sensor entity."""
native_value_fn: Callable[[str], float] = float
state_class = SensorStateClass.MEASUREMENT
SENSOR_DESCRIPTIONS = [
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key="BME280_humidity",
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BME280_pressure",
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.PA,
suggested_unit_of_measurement=UnitOfPressure.MMHG,
suggested_display_precision=0,
translation_placeholders={"sensor_name": "BME280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="BME280_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BMP_pressure",
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.PA,
suggested_unit_of_measurement=UnitOfPressure.MMHG,
suggested_display_precision=0,
translation_placeholders={"sensor_name": "BMP"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="BMP_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BMP"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="BMP280_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BMP280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BMP280_pressure",
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.PA,
suggested_unit_of_measurement=UnitOfPressure.MMHG,
suggested_display_precision=0,
translation_placeholders={"sensor_name": "BMP280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key="HTU21D_humidity",
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "HTU21D"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="HTU21D_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "HTU21D"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PM10,
translation_key="pm_10",
key="SDS_P1",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
suggested_display_precision=2,
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PM25,
translation_key="pm_25",
key="SDS_P2",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
suggested_display_precision=2,
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key="SHT3X_humidity",
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "SHT3X"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="SHT3X_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "SHT3X"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
key="signal",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.SOUND_PRESSURE,
key="PCBA_noiseMax",
translation_key="noise_max",
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
suggested_display_precision=0,
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.SOUND_PRESSURE,
key="PCBA_noiseAvg",
translation_key="noise_avg",
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
suggested_display_precision=0,
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
translation_key="co2",
key="CCS_CO2",
suggested_display_precision=2,
translation_placeholders={"sensor_name": "CCS"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
key="CCS_TVOC",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
suggested_display_precision=2,
),
AltruistSensorEntityDescription(
key="GC",
native_unit_of_measurement="μR/h",
translation_key="radiation",
suggested_display_precision=2,
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.CO2,
translation_key="co2",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
key="SCD4x_co2",
suggested_display_precision=2,
translation_placeholders={"sensor_name": "SCD4x"},
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AltruistConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add sensors for passed config_entry in HA."""
coordinator = config_entry.runtime_data
async_add_entities(
AltruistSensor(coordinator, sensor_description)
for sensor_description in SENSOR_DESCRIPTIONS
if sensor_description.key in coordinator.data
)
class AltruistSensor(CoordinatorEntity[AltruistDataUpdateCoordinator], SensorEntity):
"""Implementation of a Altruist sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AltruistDataUpdateCoordinator,
description: AltruistSensorEntityDescription,
) -> None:
"""Initialize the Altruist sensor."""
super().__init__(coordinator)
self._device = coordinator.client.device
self.entity_description: AltruistSensorEntityDescription = description
self._attr_unique_id = f"{self._device.id}-{description.key}"
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, self._device.id)},
manufacturer="Robonomics",
model="Altruist",
sw_version=self._device.fw_version,
configuration_url=f"http://{self._device.ip_address}",
serial_number=self._device.id,
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available and self.entity_description.key in self.coordinator.data
)
@property
def native_value(self) -> float | int:
"""Return the native value of the sensor."""
string_value = self.coordinator.data[self.entity_description.key]
return self.entity_description.native_value_fn(string_value)

View File

@ -0,0 +1,51 @@
{
"config": {
"flow_title": "{name}",
"step": {
"discovery_confirm": {
"description": "Do you want to start setup {model}?"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "Altruist IP address or hostname in the local network"
},
"description": "Fill in Altruist IP address or hostname in your local network"
}
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"no_device_found": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"entity": {
"sensor": {
"humidity": {
"name": "{sensor_name} humidity"
},
"pressure": {
"name": "{sensor_name} pressure"
},
"temperature": {
"name": "{sensor_name} temperature"
},
"noise_max": {
"name": "Maximum noise"
},
"noise_avg": {
"name": "Average noise"
},
"co2": {
"name": "{sensor_name} CO2"
},
"radiation": {
"name": "Radiation level"
}
}
}
}

View File

@ -11,6 +11,6 @@
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"quality_scale": "platinum",
"requirements": ["bosch-alarm-mode2==0.4.6"]
}

View File

@ -28,38 +28,41 @@ rules:
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
docs-configuration-parameters:
status: exempt
comment: |
No options flow is provided.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
Device type integration
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |

View File

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

View File

@ -9,7 +9,10 @@ import voluptuous as vol
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import ConditionProtocol, trace_condition_function
from homeassistant.helpers.condition import (
ConditionCheckerType,
trace_condition_function,
)
from homeassistant.helpers.typing import ConfigType
from . import DeviceAutomationType, async_get_device_automation_platform
@ -19,13 +22,24 @@ if TYPE_CHECKING:
from homeassistant.helpers import condition
class DeviceAutomationConditionProtocol(ConditionProtocol, Protocol):
class DeviceAutomationConditionProtocol(Protocol):
"""Define the format of device_condition modules.
Each module must define either CONDITION_SCHEMA or async_validate_condition_config
from ConditionProtocol.
Each module must define either CONDITION_SCHEMA or async_validate_condition_config.
"""
CONDITION_SCHEMA: vol.Schema
async def async_validate_condition_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
def async_condition_from_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
"""Evaluate state based on configuration."""
async def async_get_condition_capabilities(
self, hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:

View File

@ -65,6 +65,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
"/ivp/ensemble/generator",
"/ivp/meters",
"/ivp/meters/readings",
"/ivp/pdm/device_data",
"/home",
]

View File

@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.0.1"],
"requirements": ["pyenphase==2.1.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@ -45,6 +45,7 @@ from homeassistant.const import (
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@ -80,6 +81,114 @@ INVERTER_SENSORS = (
device_class=SensorDeviceClass.POWER,
value_fn=attrgetter("last_report_watts"),
),
EnvoyInverterSensorEntityDescription(
key="dc_voltage",
translation_key="dc_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("dc_voltage"),
),
EnvoyInverterSensorEntityDescription(
key="dc_current",
translation_key="dc_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("dc_current"),
),
EnvoyInverterSensorEntityDescription(
key="ac_voltage",
translation_key="ac_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("ac_voltage"),
),
EnvoyInverterSensorEntityDescription(
key="ac_current",
translation_key="ac_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("ac_current"),
),
EnvoyInverterSensorEntityDescription(
key="ac_frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("ac_frequency"),
),
EnvoyInverterSensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=3,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=attrgetter("temperature"),
),
EnvoyInverterSensorEntityDescription(
key="lifetime_energy",
translation_key="lifetime_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=attrgetter("lifetime_energy"),
),
EnvoyInverterSensorEntityDescription(
key="energy_today",
translation_key="energy_today",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=attrgetter("energy_today"),
),
EnvoyInverterSensorEntityDescription(
key="last_report_duration",
translation_key="last_report_duration",
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=attrgetter("last_report_duration"),
),
EnvoyInverterSensorEntityDescription(
key="energy_produced",
translation_key="energy_produced",
native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("energy_produced"),
),
EnvoyInverterSensorEntityDescription(
key="max_reported",
translation_key="max_reported",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=attrgetter("max_report_watts"),
),
EnvoyInverterSensorEntityDescription(
key=LAST_REPORTED_KEY,
translation_key=LAST_REPORTED_KEY,

View File

@ -379,7 +379,34 @@
"name": "Aggregated Battery capacity"
},
"aggregated_soc": {
"name": "Aggregated battery soc"
"name": "Aggregated battery SOC"
},
"dc_voltage": {
"name": "DC voltage"
},
"dc_current": {
"name": "DC current"
},
"ac_voltage": {
"name": "AC voltage"
},
"ac_current": {
"name": "AC current"
},
"lifetime_energy": {
"name": "[%key:component::enphase_envoy::entity::sensor::lifetime_production::name%]"
},
"energy_today": {
"name": "[%key:component::enphase_envoy::entity::sensor::daily_production::name%]"
},
"energy_produced": {
"name": "Energy production since previous report"
},
"max_reported": {
"name": "Lifetime maximum power"
},
"last_report_duration": {
"name": "Last report duration"
}
},
"switch": {

View File

@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==33.0.0",
"aioesphomeapi==33.1.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
],

View File

@ -1,6 +1,6 @@
"""The foscam component."""
from libpyfoscam import FoscamCamera
from libpyfoscamcgi import FoscamCamera
from homeassistant.const import (
CONF_HOST,

View File

@ -2,8 +2,8 @@
from typing import Any
from libpyfoscam import FoscamCamera
from libpyfoscam.foscam import (
from libpyfoscamcgi import FoscamCamera
from libpyfoscamcgi.foscamcgi import (
ERROR_FOSCAM_AUTH,
ERROR_FOSCAM_UNAVAILABLE,
FOSCAM_SUCCESS,

View File

@ -4,7 +4,7 @@ import asyncio
from datetime import timedelta
from typing import Any
from libpyfoscam import FoscamCamera
from libpyfoscamcgi import FoscamCamera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

View File

@ -5,6 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/foscam",
"iot_class": "local_polling",
"loggers": ["libpyfoscam"],
"requirements": ["libpyfoscam==1.2.2"]
"loggers": ["libpyfoscamcgi"],
"requirements": ["libpyfoscamcgi==0.0.6"]
}

View File

@ -12,7 +12,7 @@ from google.genai.types import File, FileState
from requests.exceptions import Timeout
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import (
HomeAssistant,
@ -26,7 +26,11 @@ from homeassistant.exceptions import (
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
@ -56,6 +60,8 @@ type GoogleGenerativeAIConfigEntry = ConfigEntry[Client]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Google Generative AI Conversation."""
await async_migrate_integration(hass)
async def generate_content(call: ServiceCall) -> ServiceResponse:
"""Generate content from text and optionally images."""
@ -209,3 +215,68 @@ async def async_unload_entry(
return False
return True
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""
entries = hass.config_entries.async_entries(DOMAIN)
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, ConfigEntry] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
for entry in entries:
use_existing = False
subentry = ConfigSubentry(
data=entry.options,
subentry_type="conversation",
title=entry.title,
unique_id=None,
)
if entry.data[CONF_API_KEY] not in api_keys_entries:
use_existing = True
api_keys_entries[entry.data[CONF_API_KEY]] = entry
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
hass.config_entries.async_add_subentry(parent_entry, subentry)
conversation_entity = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
entry.entry_id,
)
if conversation_entity is not None:
entity_registry.async_update_entity(
conversation_entity,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
new_unique_id=subentry.subentry_id,
)
device = device_registry.async_get_device(
identifiers={(DOMAIN, entry.entry_id)}
)
if device is not None:
device_registry.async_update_device(
device.id,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_id,
add_config_entry_id=parent_entry.entry_id,
)
if parent_entry.entry_id != entry.entry_id:
device_registry.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
)
if not use_existing:
await hass.config_entries.async_remove(entry.entry_id)
else:
hass.config_entries.async_update_entry(
entry,
options={},
version=2,
)

View File

@ -4,8 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
import logging
from types import MappingProxyType
from typing import Any
from typing import Any, cast
from google import genai
from google.genai.errors import APIError, ClientError
@ -15,12 +14,14 @@ import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
ConfigSubentryFlow,
SubentryFlowResult,
)
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import llm
from homeassistant.helpers.selector import (
NumberSelector,
@ -45,6 +46,7 @@ from .const import (
CONF_TOP_K,
CONF_TOP_P,
CONF_USE_GOOGLE_SEARCH_TOOL,
DEFAULT_CONVERSATION_NAME,
DOMAIN,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_HARM_BLOCK_THRESHOLD,
@ -66,7 +68,7 @@ STEP_API_DATA_SCHEMA = vol.Schema(
RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
@ -90,7 +92,7 @@ async def validate_input(data: dict[str, Any]) -> None:
class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Google Generative AI Conversation."""
VERSION = 1
VERSION = 2
async def async_step_api(
self, user_input: dict[str, Any] | None = None
@ -98,6 +100,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(user_input)
try:
await validate_input(user_input)
except (APIError, Timeout) as err:
@ -117,7 +120,14 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title="Google Generative AI",
data=user_input,
options=RECOMMENDED_OPTIONS,
subentries=[
{
"subentry_type": "conversation",
"data": RECOMMENDED_OPTIONS,
"title": DEFAULT_CONVERSATION_NAME,
"unique_id": None,
}
],
)
return self.async_show_form(
step_id="api",
@ -156,41 +166,72 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
@staticmethod
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
return GoogleGenerativeAIOptionsFlow(config_entry)
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: ConfigEntry
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this integration."""
return {"conversation": ConversationSubentryFlowHandler}
class GoogleGenerativeAIOptionsFlow(OptionsFlow):
"""Google Generative AI config flow options handler."""
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
"""Flow for managing conversation subentries."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False
)
self._genai_client = config_entry.runtime_data
last_rendered_recommended = False
async def async_step_init(
@property
def _genai_client(self) -> genai.Client:
"""Return the Google Generative AI client."""
return self._get_entry().runtime_data
@property
def _is_new(self) -> bool:
"""Return if this is a new subentry."""
return self.source == "user"
async def async_step_set_options(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
) -> SubentryFlowResult:
"""Set conversation options."""
# abort if entry is not loaded
if self._get_entry().state != ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
errors: dict[str, str] = {}
if user_input is not None:
if user_input is None:
if self._is_new:
options = RECOMMENDED_OPTIONS.copy()
else:
# If this is a reconfiguration, we need to copy the existing options
# so that we can show the current values in the form.
options = self._get_reconfigure_subentry().data.copy()
self.last_rendered_recommended = cast(
bool, options.get(CONF_RECOMMENDED, False)
)
else:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if not user_input.get(CONF_LLM_HASS_API):
user_input.pop(CONF_LLM_HASS_API, None)
# Don't allow to save options that enable the Google Seearch tool with an Assist API
if not (
user_input.get(CONF_LLM_HASS_API)
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
):
# Don't allow to save options that enable the Google Seearch tool with an Assist API
return self.async_create_entry(title="", data=user_input)
if self._is_new:
return self.async_create_entry(
title=user_input.pop(CONF_NAME),
data=user_input,
)
return self.async_update_and_abort(
self._get_entry(),
self._get_reconfigure_subentry(),
data=user_input,
)
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
# Re-render the options again, now with the recommended options shown/hidden
@ -199,15 +240,19 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
options = user_input
schema = await google_generative_ai_config_option_schema(
self.hass, options, self._genai_client
self.hass, self._is_new, options, self._genai_client
)
return self.async_show_form(
step_id="init", data_schema=vol.Schema(schema), errors=errors
step_id="set_options", data_schema=vol.Schema(schema), errors=errors
)
async_step_reconfigure = async_step_set_options
async_step_user = async_step_set_options
async def google_generative_ai_config_option_schema(
hass: HomeAssistant,
is_new: bool,
options: Mapping[str, Any],
genai_client: genai.Client,
) -> dict:
@ -224,7 +269,15 @@ async def google_generative_ai_config_option_schema(
):
suggested_llm_apis = [suggested_llm_apis]
schema = {
if is_new:
schema: dict[vol.Required | vol.Optional, Any] = {
vol.Required(CONF_NAME, default=DEFAULT_CONVERSATION_NAME): str,
}
else:
schema = {}
schema.update(
{
vol.Optional(
CONF_PROMPT,
description={
@ -241,6 +294,7 @@ async def google_generative_ai_config_option_schema(
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
): bool,
}
)
if options.get(CONF_RECOMMENDED):
return schema

View File

@ -6,10 +6,12 @@ DOMAIN = "google_generative_ai_conversation"
LOGGER = logging.getLogger(__package__)
CONF_PROMPT = "prompt"
DEFAULT_CONVERSATION_NAME = "Google AI Conversation"
ATTR_MODEL = "model"
CONF_RECOMMENDED = "recommended"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "models/gemini-2.0-flash"
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
RECOMMENDED_TTS_MODEL = "gemini-2.5-flash-preview-tts"
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Literal
from homeassistant.components import assist_pipeline, conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -22,8 +22,14 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up conversation entities."""
agent = GoogleGenerativeAIConversationEntity(config_entry)
async_add_entities([agent])
for subentry in config_entry.subentries.values():
if subentry.subentry_type != "conversation":
continue
async_add_entities(
[GoogleGenerativeAIConversationEntity(config_entry, subentry)],
config_subentry_id=subentry.subentry_id,
)
class GoogleGenerativeAIConversationEntity(
@ -35,10 +41,10 @@ class GoogleGenerativeAIConversationEntity(
_attr_supports_streaming = True
def __init__(self, entry: ConfigEntry) -> None:
def __init__(self, entry: ConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the agent."""
super().__init__(entry)
if self.entry.options.get(CONF_LLM_HASS_API):
super().__init__(entry, subentry)
if self.subentry.data.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
)
@ -70,7 +76,7 @@ class GoogleGenerativeAIConversationEntity(
chat_log: conversation.ChatLog,
) -> conversation.ConversationResult:
"""Call the API."""
options = self.entry.options
options = self.subentry.data
try:
await chat_log.async_provide_llm_data(

View File

@ -24,7 +24,7 @@ from google.genai.types import (
from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
@ -301,17 +301,16 @@ async def _transform_stream(
class GoogleGenerativeAILLMBaseEntity(Entity):
"""Google Generative AI base entity."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, entry: ConfigEntry) -> None:
def __init__(self, entry: ConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the agent."""
self.entry = entry
self.subentry = subentry
self._attr_name = subentry.title
self._genai_client = entry.runtime_data
self._attr_unique_id = entry.entry_id
self._attr_unique_id = subentry.subentry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=entry.title,
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Google",
model="Generative AI",
entry_type=dr.DeviceEntryType.SERVICE,
@ -322,7 +321,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
chat_log: conversation.ChatLog,
) -> None:
"""Generate an answer for the chat log."""
options = self.entry.options
options = self.subentry.data
tools: list[Tool | Callable[..., Any]] | None = None
if chat_log.llm_api:

View File

@ -18,13 +18,22 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"options": {
"config_subentries": {
"conversation": {
"initiate_flow": {
"user": "Add conversation agent",
"reconfigure": "Reconfigure conversation agent"
},
"entry_type": "Conversation agent",
"step": {
"init": {
"set_options": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"recommended": "Recommended model settings",
"prompt": "Instructions",
"chat_model": "[%key:common::generic::model%]",
@ -45,9 +54,14 @@
}
}
},
"abort": {
"entry_not_loaded": "Cannot add things while the configuration is disabled.",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"invalid_google_search_option": "Google Search can only be enabled if nothing is selected in the \"Control Home Assistant\" setting."
}
}
},
"services": {
"generate_content": {

View File

@ -113,7 +113,6 @@ class GoogleGenerativeAITextToSpeechEntity(TextToSpeechEntity):
self._attr_unique_id = f"{entry.entry_id}_tts"
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=entry.title,
manufacturer="Google",
model="Generative AI",
entry_type=dr.DeviceEntryType.SERVICE,

View File

@ -12,7 +12,6 @@ import re
import struct
from typing import Any, NamedTuple
import aiofiles
from aiohasupervisor import SupervisorError
import voluptuous as vol
@ -239,12 +238,6 @@ def _is_32_bit() -> bool:
return size * 8 == 32
async def _get_arch() -> str:
async with aiofiles.open("/etc/apk/arch") as arch_file:
raw_arch = await arch_file.read()
return {"x86": "i386"}.get(raw_arch, raw_arch)
class APIEndpointSettings(NamedTuple):
"""Settings for API endpoint."""
@ -566,8 +559,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data[ADDONS_COORDINATOR] = coordinator
arch = await _get_arch()
def deprecated_setup_issue() -> None:
os_info = get_os_info(hass)
info = get_info(hass)
@ -575,6 +566,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return
is_haos = info.get("hassos") is not None
board = os_info.get("board")
arch = info.get("arch", "unknown")
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
unsupported_os_on_board = board in {"rpi3", "rpi4"}
if is_haos and (unsupported_board or unsupported_os_on_board):

View File

@ -7,7 +7,6 @@ import logging
import struct
from typing import Any
import aiofiles
import voluptuous as vol
from homeassistant import config as conf_util, core_config
@ -18,6 +17,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_LATITUDE,
ATTR_LONGITUDE,
EVENT_HOMEASSISTANT_STARTED,
RESTART_EXIT_CODE,
SERVICE_RELOAD,
SERVICE_SAVE_PERSISTENT_STATES,
@ -26,6 +26,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
)
from homeassistant.core import (
Event,
HomeAssistant,
ServiceCall,
ServiceResponse,
@ -101,12 +102,6 @@ def _is_32_bit() -> bool:
return size * 8 == 32
async def _get_arch() -> str:
async with aiofiles.open("/etc/apk/arch") as arch_file:
raw_arch = (await arch_file.read()).strip()
return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
"""Set up general services related to Home Assistant."""
@ -411,6 +406,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
async_set_stop_handler(hass, _async_stop)
async def _async_check_deprecation(event: Event) -> None:
"""Check and create deprecation issues after startup."""
info = await async_get_system_info(hass)
installation_type = info["installation_type"][15:]
@ -419,7 +416,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
bit32 = _is_32_bit()
arch = info["arch"]
if bit32 and installation_type == "Container":
arch = await _get_arch()
arch = info.get("container_arch", arch)
ir.async_create_issue(
hass,
DOMAIN,
@ -451,6 +448,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
},
)
# Delay deprecation check to make sure installation method is determined correctly
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_check_deprecation)
return True

View File

@ -124,6 +124,7 @@
"info": {
"arch": "CPU architecture",
"config_dir": "Configuration directory",
"container_arch": "Container architecture",
"dev": "Development",
"docker": "Docker",
"hassio": "Supervisor",

View File

@ -27,6 +27,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"dev": info.get("dev"),
"hassio": info.get("hassio"),
"docker": info.get("docker"),
"container_arch": info.get("container_arch"),
"user": info.get("user"),
"virtualenv": info.get("virtualenv"),
"python_version": info.get("python_version"),

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system",
"requirements": [
"universal-silabs-flasher==0.0.30",
"universal-silabs-flasher==0.0.31",
"ha-silabs-firmware-client==0.2.0"
]
}

View File

@ -12,6 +12,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==9.1.1"],
"requirements": ["python-homewizard-energy==9.2.0"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}

View File

@ -24,7 +24,7 @@
},
"authorize": {
"title": "Authorize",
"description": "Press the button on the HomeWizard Energy device, then select the button below."
"description": "Press the button on the HomeWizard Energy device for two seconds, then select the button below."
},
"reconfigure": {
"description": "Update configuration for {title}.",

View File

@ -1,95 +1,36 @@
"""The JuiceNet integration."""
import logging
import aiohttp
from pyjuicenet import Api, TokenError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
),
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the JuiceNet component."""
conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
if not conf:
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True
from .const import DOMAIN
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up JuiceNet from a config entry."""
config = entry.data
session = async_get_clientsession(hass)
access_token = config[CONF_ACCESS_TOKEN]
api = Api(access_token, session)
juicenet = JuiceNetApi(api)
try:
await juicenet.setup()
except TokenError as error:
_LOGGER.error("JuiceNet Error %s", error)
return False
except aiohttp.ClientError as error:
_LOGGER.error("Could not reach the JuiceNet API %s", error)
raise ConfigEntryNotReady from error
if not juicenet.devices:
_LOGGER.error("No JuiceNet devices found for this account")
return False
_LOGGER.debug("%d JuiceNet device(s) found", len(juicenet.devices))
coordinator = JuiceNetCoordinator(hass, entry, juicenet)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
JUICENET_API: juicenet,
JUICENET_COORDINATOR: coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
ir.async_create_issue(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/juicenet",
},
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
return True

View File

@ -1,82 +1,11 @@
"""Config flow for JuiceNet integration."""
import logging
from typing import Any
import aiohttp
from pyjuicenet import Api, TokenError
import voluptuous as vol
from homeassistant import core, exceptions
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.config_entries import ConfigFlow
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
session = async_get_clientsession(hass)
juicenet = Api(data[CONF_ACCESS_TOKEN], session)
try:
await juicenet.get_devices()
except TokenError as error:
_LOGGER.error("Token Error %s", error)
raise InvalidAuth from error
except aiohttp.ClientError as error:
_LOGGER.error("Error connecting %s", error)
raise CannotConnect from error
# Return info that you want to store in the config entry.
return {"title": "JuiceNet"}
class JuiceNetConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for JuiceNet."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN])
self._abort_if_unique_id_configured()
try:
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info["title"], data=user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle import."""
return await self.async_step_user(import_data)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -1,6 +1,3 @@
"""Constants used by the JuiceNet component."""
DOMAIN = "juicenet"
JUICENET_API = "juicenet_api"
JUICENET_COORDINATOR = "juicenet_coordinator"

View File

@ -1,33 +0,0 @@
"""The JuiceNet integration."""
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .device import JuiceNetApi
_LOGGER = logging.getLogger(__name__)
class JuiceNetCoordinator(DataUpdateCoordinator[None]):
"""Coordinator for JuiceNet."""
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, juicenet_api: JuiceNetApi
) -> None:
"""Initialize the JuiceNet coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name="JuiceNet",
update_interval=timedelta(seconds=30),
)
self.juicenet_api = juicenet_api
async def _async_update_data(self) -> None:
for device in self.juicenet_api.devices:
await device.update_state(True)

View File

@ -1,21 +0,0 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""
from pyjuicenet import Api, Charger
class JuiceNetApi:
"""Represent a connection to JuiceNet."""
def __init__(self, api: Api) -> None:
"""Create an object from the provided API instance."""
self.api = api
self._devices: list[Charger] = []
async def setup(self) -> None:
"""JuiceNet device setup."""
self._devices = await self.api.get_devices()
@property
def devices(self) -> list[Charger]:
"""Get a list of devices managed by this account."""
return self._devices

View File

@ -1,32 +0,0 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""
from pyjuicenet import Charger
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import JuiceNetCoordinator
class JuiceNetEntity(CoordinatorEntity[JuiceNetCoordinator]):
"""Represent a base JuiceNet device."""
_attr_has_entity_name = True
def __init__(
self, device: Charger, key: str, coordinator: JuiceNetCoordinator
) -> None:
"""Initialise the sensor."""
super().__init__(coordinator)
self.device = device
self.key = key
self._attr_unique_id = f"{device.id}-{key}"
self._attr_device_info = DeviceInfo(
configuration_url=(
f"https://home.juice.net/Portal/Details?unitID={device.id}"
),
identifiers={(DOMAIN, device.id)},
manufacturer="JuiceNet",
name=device.name,
)

View File

@ -1,10 +1,9 @@
{
"domain": "juicenet",
"name": "JuiceNet",
"codeowners": ["@jesserockz"],
"config_flow": true,
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/juicenet",
"integration_type": "system",
"iot_class": "cloud_polling",
"loggers": ["pyjuicenet"],
"requirements": ["python-juicenet==1.1.0"]
"requirements": []
}

View File

@ -1,93 +0,0 @@
"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers."""
from __future__ import annotations
from dataclasses import dataclass
from pyjuicenet import Charger
from homeassistant.components.number import (
DEFAULT_MAX_VALUE,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
from .entity import JuiceNetEntity
@dataclass(frozen=True, kw_only=True)
class JuiceNetNumberEntityDescription(NumberEntityDescription):
"""An entity description for a JuiceNetNumber."""
setter_key: str
native_max_value_key: str | None = None
NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = (
JuiceNetNumberEntityDescription(
translation_key="amperage_limit",
key="current_charging_amperage_limit",
native_min_value=6,
native_max_value_key="max_charging_amperage",
native_step=1,
setter_key="set_charging_amperage_limit",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the JuiceNet Numbers."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api: JuiceNetApi = juicenet_data[JUICENET_API]
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
entities = [
JuiceNetNumber(device, description, coordinator)
for device in api.devices
for description in NUMBER_TYPES
]
async_add_entities(entities)
class JuiceNetNumber(JuiceNetEntity, NumberEntity):
"""Implementation of a JuiceNet number."""
entity_description: JuiceNetNumberEntityDescription
def __init__(
self,
device: Charger,
description: JuiceNetNumberEntityDescription,
coordinator: JuiceNetCoordinator,
) -> None:
"""Initialise the number."""
super().__init__(device, description.key, coordinator)
self.entity_description = description
@property
def native_value(self) -> float | None:
"""Return the value of the entity."""
return getattr(self.device, self.entity_description.key, None)
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
if self.entity_description.native_max_value_key is not None:
return getattr(self.device, self.entity_description.native_max_value_key)
if self.entity_description.native_max_value is not None:
return self.entity_description.native_max_value
return DEFAULT_MAX_VALUE
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await getattr(self.device, self.entity_description.setter_key)(value)

View File

@ -1,124 +0,0 @@
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors."""
from __future__ import annotations
from pyjuicenet import Charger
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
from .entity import JuiceNetEntity
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="status",
name="Charging Status",
),
SensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
),
SensorEntityDescription(
key="amps",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="watts",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="charge_time",
translation_key="charge_time",
native_unit_of_measurement=UnitOfTime.SECONDS,
icon="mdi:timer-outline",
),
SensorEntityDescription(
key="energy_added",
translation_key="energy_added",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the JuiceNet Sensors."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api: JuiceNetApi = juicenet_data[JUICENET_API]
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
entities = [
JuiceNetSensorDevice(device, coordinator, description)
for device in api.devices
for description in SENSOR_TYPES
]
async_add_entities(entities)
class JuiceNetSensorDevice(JuiceNetEntity, SensorEntity):
"""Implementation of a JuiceNet sensor."""
def __init__(
self,
device: Charger,
coordinator: JuiceNetCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialise the sensor."""
super().__init__(device, description.key, coordinator)
self.entity_description = description
@property
def icon(self):
"""Return the icon of the sensor."""
icon = None
if self.entity_description.key == "status":
status = self.device.status
if status == "standby":
icon = "mdi:power-plug-off"
elif status == "plugged":
icon = "mdi:power-plug"
elif status == "charging":
icon = "mdi:battery-positive"
else:
icon = self.entity_description.icon
return icon
@property
def native_value(self):
"""Return the state."""
return getattr(self.device, self.entity_description.key, None)

View File

@ -1,41 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
},
"description": "You will need the API Token from https://home.juice.net/Manage.",
"title": "Connect to JuiceNet"
}
}
},
"entity": {
"number": {
"amperage_limit": {
"name": "Amperage limit"
}
},
"sensor": {
"charge_time": {
"name": "Charge time"
},
"energy_added": {
"name": "Energy added"
}
},
"switch": {
"charge_now": {
"name": "Charge now"
}
"issues": {
"integration_removed": {
"title": "The JuiceNet integration has been removed",
"description": "Enel X has dropped support for JuiceNet in favor of JuicePass, and the JuiceNet integration has been removed from Home Assistant as it was no longer working.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing JuiceNet integration entries]({entries})."
}
}
}

View File

@ -1,53 +0,0 @@
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches."""
from typing import Any
from pyjuicenet import Charger
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
from .entity import JuiceNetEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the JuiceNet switches."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api: JuiceNetApi = juicenet_data[JUICENET_API]
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
async_add_entities(
JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices
)
class JuiceNetChargeNowSwitch(JuiceNetEntity, SwitchEntity):
"""Implementation of a JuiceNet switch."""
_attr_translation_key = "charge_now"
def __init__(self, device: Charger, coordinator: JuiceNetCoordinator) -> None:
"""Initialise the switch."""
super().__init__(device, "charge_now", coordinator)
@property
def is_on(self):
"""Return true if switch is on."""
return self.device.override_time != 0
async def async_turn_on(self, **kwargs: Any) -> None:
"""Charge now."""
await self.device.set_override(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Don't charge now."""
await self.device.set_override(False)

View File

@ -146,17 +146,27 @@ class KeeneticOptionsFlowHandler(OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
if (
not hasattr(self.config_entry, "runtime_data")
or not self.config_entry.runtime_data
):
return self.async_abort(reason="not_initialized")
router = self.config_entry.runtime_data
try:
interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
router.client.get_interfaces
)
except ConnectionException:
return self.async_abort(reason="cannot_connect")
self._interface_options = {
interface.name: (interface.description or interface.name)
for interface in interfaces
if interface.type.lower() == "bridge"
}
return await self.async_step_user()
async def async_step_user(
@ -182,9 +192,13 @@ class KeeneticOptionsFlowHandler(OptionsFlow):
): int,
vol.Required(
CONF_INTERFACES,
default=self.config_entry.options.get(
default=[
item
for item in self.config_entry.options.get(
CONF_INTERFACES, [DEFAULT_INTERFACE]
),
)
if item in self._interface_options
],
): cv.multi_select(self._interface_options),
vol.Optional(
CONF_TRY_HOTSPOT,

View File

@ -36,6 +36,10 @@
"include_associated": "Use Wi-Fi AP associations data (ignored if hotspot data used)"
}
}
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"not_initialized": "The integration is not initialized yet. Can't display available options."
}
}
}

View File

@ -9,6 +9,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["xknx", "xknxproject"],
"quality_scale": "silver",
"requirements": [
"xknx==3.8.0",
"xknxproject==3.8.2",

View File

@ -13,7 +13,7 @@ rules:
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: todo
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name:
@ -41,8 +41,8 @@ rules:
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable:
@ -64,21 +64,24 @@ rules:
comment: |
YAML entities don't support devices. UI entities support user-defined devices.
diagnostics: done
discovery-update-info: todo
discovery-update-info:
status: exempt
comment: |
KNX doesn't support any provided discovery method.
discovery:
status: exempt
comment: |
KNX doesn't support any provided discovery method.
docs-data-update: todo
docs-data-update: done
docs-examples: done
docs-known-limitations: todo
docs-known-limitations: done
docs-supported-devices:
status: exempt
comment: |
Devices aren't supported directly since communication is on group address level.
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |

View File

@ -13,6 +13,7 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
LOGGER = logging.getLogger(__package__)

View File

@ -3,6 +3,7 @@
from __future__ import annotations
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
@ -21,10 +22,13 @@ class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator]):
def __init__(self, coordinator: LaMetricDataUpdateCoordinator) -> None:
"""Initialize the LaMetric entity."""
super().__init__(coordinator=coordinator)
connections = {(CONNECTION_NETWORK_MAC, format_mac(coordinator.data.wifi.mac))}
if coordinator.data.bluetooth is not None:
connections.add(
(CONNECTION_BLUETOOTH, format_mac(coordinator.data.bluetooth.address))
)
self._attr_device_info = DeviceInfo(
connections={
(CONNECTION_NETWORK_MAC, format_mac(coordinator.data.wifi.mac))
},
connections=connections,
identifiers={(DOMAIN, coordinator.data.serial_number)},
manufacturer="LaMetric Inc.",
model_id=coordinator.data.model,

View File

@ -0,0 +1,46 @@
"""LaMetric Update platform."""
from awesomeversion import AwesomeVersion
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: LaMetricConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LaMetric update platform."""
coordinator = config_entry.runtime_data
if coordinator.data.os_version >= AwesomeVersion("2.3.0"):
async_add_entities([LaMetricUpdate(coordinator)])
class LaMetricUpdate(LaMetricEntity, UpdateEntity):
"""Representation of LaMetric Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
def __init__(self, coordinator: LaMetricDataUpdateCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.data.serial_number}-update"
@property
def installed_version(self) -> str:
"""Return the installed version of the entity."""
return self.coordinator.data.os_version
@property
def latest_version(self) -> str | None:
"""Return the latest version of the entity."""
if not self.coordinator.data.update:
return None
return self.coordinator.data.update.version

View File

@ -25,6 +25,8 @@ from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, DOMAIN, SETPOINTS
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
def add_lcn_entities(
config_entry: LcnConfigEntry,

View File

@ -38,6 +38,8 @@ from .const import (
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
DEVICE_CLASS_MAPPING = {
pypck.lcn_defs.VarUnit.CELSIUS: SensorDeviceClass.TEMPERATURE,
pypck.lcn_defs.VarUnit.KELVIN: SensorDeviceClass.TEMPERATURE,

View File

@ -13,7 +13,6 @@ from aiolifx.connection import LIFXConnection
import voluptuous as vol
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
@ -27,7 +26,7 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from homeassistant.helpers.typing import ConfigType
from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN, TARGET_ANY
from .coordinator import LIFXUpdateCoordinator
from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .discovery import async_discover_devices, async_trigger_discovery
from .manager import LIFXManager
from .migration import async_migrate_entities_devices, async_migrate_legacy_entries
@ -73,7 +72,7 @@ DISCOVERY_COOLDOWN = 5
async def async_legacy_migration(
hass: HomeAssistant,
legacy_entry: ConfigEntry,
legacy_entry: LIFXConfigEntry,
discovered_devices: Iterable[Light],
) -> bool:
"""Migrate config entries."""
@ -157,7 +156,6 @@ class LIFXDiscoveryManager:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIFX component."""
hass.data[DOMAIN] = {}
migrating = bool(async_get_legacy_entry(hass))
discovery_manager = LIFXDiscoveryManager(hass, migrating)
@ -187,7 +185,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> bool:
"""Set up LIFX from a config entry."""
if async_entry_is_legacy(entry):
return True
@ -198,10 +196,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async_migrate_entities_devices(hass, legacy_entry.entry_id, entry)
assert entry.unique_id is not None
domain_data = hass.data[DOMAIN]
if DATA_LIFX_MANAGER not in domain_data:
if DATA_LIFX_MANAGER not in hass.data:
manager = LIFXManager(hass)
domain_data[DATA_LIFX_MANAGER] = manager
hass.data[DATA_LIFX_MANAGER] = manager
manager.async_setup()
host = entry.data[CONF_HOST]
@ -229,21 +226,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady(
f"Unexpected device found at {host}; expected {entry.unique_id}, found {serial}"
)
domain_data[entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> bool:
"""Unload a config entry."""
if async_entry_is_legacy(entry):
return True
domain_data = hass.data[DOMAIN]
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
coordinator: LIFXUpdateCoordinator = domain_data.pop(entry.entry_id)
coordinator.connection.async_stop()
entry.runtime_data.connection.async_stop()
# Only the DATA_LIFX_MANAGER left, remove it.
if len(domain_data) == 1:
manager: LIFXManager = domain_data.pop(DATA_LIFX_MANAGER)
if len(hass.config_entries.async_loaded_entries(DOMAIN)) == 0:
manager = hass.data.pop(DATA_LIFX_MANAGER)
manager.async_unload()
return unload_ok

View File

@ -7,13 +7,12 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, HEV_CYCLE_STATE
from .coordinator import LIFXUpdateCoordinator
from .const import HEV_CYCLE_STATE
from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
from .util import lifx_features
@ -27,11 +26,11 @@ HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: LIFXConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LIFX from a config entry."""
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
if lifx_features(coordinator.device)["hev"]:
async_add_entities(

View File

@ -7,13 +7,12 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, IDENTIFY, RESTART
from .coordinator import LIFXUpdateCoordinator
from .const import IDENTIFY, RESTART
from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription(
@ -31,12 +30,11 @@ IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: LIFXConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LIFX from a config entry."""
domain_data = hass.data[DOMAIN]
coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
[LIFXRestartButton(coordinator), LIFXIdentifyButton(coordinator)]
)

View File

@ -1,8 +1,17 @@
"""Const for LIFX."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from .manager import LIFXManager
DOMAIN = "lifx"
DATA_LIFX_MANAGER: HassKey[LIFXManager] = HassKey(DOMAIN)
TARGET_ANY = "00:00:00:00:00:00"
@ -59,7 +68,6 @@ INFRARED_BRIGHTNESS_VALUES_MAP = {
32767: "50%",
65535: "100%",
}
DATA_LIFX_MANAGER = "lifx_manager"
LIFX_CEILING_PRODUCT_IDS = {176, 177, 201, 202}

View File

@ -65,6 +65,8 @@ ZONES_PER_COLOR_UPDATE_REQUEST = 8
RSSI_DBM_FW = AwesomeVersion("2.77")
type LIFXConfigEntry = ConfigEntry[LIFXUpdateCoordinator]
class FirmwareEffect(IntEnum):
"""Enumeration of LIFX firmware effects."""
@ -87,12 +89,12 @@ class SkyType(IntEnum):
class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
"""DataUpdateCoordinator to gather data for a specific lifx device."""
config_entry: ConfigEntry
config_entry: LIFXConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LIFXConfigEntry,
connection: LIFXConnection,
) -> None:
"""Initialize DataUpdateCoordinator."""

View File

@ -5,21 +5,20 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_MAC
from homeassistant.core import HomeAssistant
from .const import CONF_LABEL, DOMAIN
from .coordinator import LIFXUpdateCoordinator
from .const import CONF_LABEL
from .coordinator import LIFXConfigEntry
TO_REDACT = [CONF_LABEL, CONF_HOST, CONF_IP_ADDRESS, CONF_MAC]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: LIFXConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a LIFX config entry."""
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
return {
"entry": {
"title": entry.title,

View File

@ -17,7 +17,6 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -37,7 +36,7 @@ from .const import (
INFRARED_BRIGHTNESS,
LIFX_CEILING_PRODUCT_IDS,
)
from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
from .coordinator import FirmwareEffect, LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
from .manager import (
SERVICE_EFFECT_COLORLOOP,
@ -78,13 +77,12 @@ HSBK_KELVIN = 3
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: LIFXConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LIFX from a config entry."""
domain_data = hass.data[DOMAIN]
coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id]
manager: LIFXManager = domain_data[DATA_LIFX_MANAGER]
coordinator = entry.runtime_data
manager = hass.data[DATA_LIFX_MANAGER]
device = coordinator.device
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
@ -123,7 +121,7 @@ class LIFXLight(LIFXEntity, LightEntity):
self,
coordinator: LIFXUpdateCoordinator,
manager: LIFXManager,
entry: ConfigEntry,
entry: LIFXConfigEntry,
) -> None:
"""Initialize the light."""
super().__init__(coordinator)

View File

@ -30,8 +30,8 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_extract_referenced_entity_ids
from .const import _ATTR_COLOR_TEMP, ATTR_THEME, DATA_LIFX_MANAGER, DOMAIN
from .coordinator import LIFXUpdateCoordinator
from .const import _ATTR_COLOR_TEMP, ATTR_THEME, DOMAIN
from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .util import convert_8_to_16, find_hsbk
if TYPE_CHECKING:
@ -494,13 +494,11 @@ class LIFXManager:
coordinators: list[LIFXUpdateCoordinator] = []
bulbs: list[Light] = []
for entry_id, coordinator in self.hass.data[DOMAIN].items():
if (
entry_id != DATA_LIFX_MANAGER
and self.entry_id_to_entity_id[entry_id] in entity_ids
):
coordinators.append(coordinator)
bulbs.append(coordinator.device)
entry: LIFXConfigEntry
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
if self.entry_id_to_entity_id[entry.entry_id] in entity_ids:
coordinators.append(entry.runtime_data)
bulbs.append(entry.runtime_data.device)
if start_effect_func := self._effect_dispatch.get(service):
await start_effect_func(self, bulbs, coordinators, **kwargs)

View File

@ -2,11 +2,11 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import LIFXConfigEntry
from .discovery import async_init_discovery_flow
@ -15,7 +15,7 @@ def async_migrate_legacy_entries(
hass: HomeAssistant,
discovered_hosts_by_serial: dict[str, str],
existing_serials: set[str],
legacy_entry: ConfigEntry,
legacy_entry: LIFXConfigEntry,
) -> int:
"""Migrate the legacy config entries to have an entry per device."""
_LOGGER.debug(
@ -45,7 +45,7 @@ def async_migrate_legacy_entries(
@callback
def async_migrate_entities_devices(
hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry
hass: HomeAssistant, legacy_entry_id: str, new_entry: LIFXConfigEntry
) -> None:
"""Move entities and devices to the new config entry."""
migrated_devices = []

View File

@ -5,18 +5,12 @@ from __future__ import annotations
from aiolifx_themes.themes import ThemeLibrary
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
ATTR_THEME,
DOMAIN,
INFRARED_BRIGHTNESS,
INFRARED_BRIGHTNESS_VALUES_MAP,
)
from .coordinator import LIFXUpdateCoordinator
from .const import ATTR_THEME, INFRARED_BRIGHTNESS, INFRARED_BRIGHTNESS_VALUES_MAP
from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
from .util import lifx_features
@ -39,11 +33,11 @@ THEME_ENTITY = SelectEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: LIFXConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LIFX from a config entry."""
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
entities: list[LIFXEntity] = []

View File

@ -10,13 +10,12 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ATTR_RSSI, DOMAIN
from .coordinator import LIFXUpdateCoordinator
from .const import ATTR_RSSI
from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
SCAN_INTERVAL = timedelta(seconds=30)
@ -33,11 +32,11 @@ RSSI_SENSOR = SensorEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: LIFXConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LIFX sensor from config entry."""
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities([LIFXRssiSensor(coordinator, RSSI_SENSOR)])

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from functools import partial
from typing import Any
from typing import TYPE_CHECKING, Any
from aiolifx import products
from aiolifx.aiolifx import Light
@ -21,7 +21,6 @@ from homeassistant.components.light import (
ATTR_RGB_COLOR,
ATTR_XY_COLOR,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.util import color as color_util
@ -35,17 +34,20 @@ from .const import (
OVERALL_TIMEOUT,
)
if TYPE_CHECKING:
from .coordinator import LIFXConfigEntry
FIX_MAC_FW = AwesomeVersion("3.70")
@callback
def async_entry_is_legacy(entry: ConfigEntry) -> bool:
def async_entry_is_legacy(entry: LIFXConfigEntry) -> bool:
"""Check if a config entry is the legacy shared one."""
return entry.unique_id is None or entry.unique_id == DOMAIN
@callback
def async_get_legacy_entry(hass: HomeAssistant) -> ConfigEntry | None:
def async_get_legacy_entry(hass: HomeAssistant) -> LIFXConfigEntry | None:
"""Get the legacy config entry."""
for entry in hass.config_entries.async_entries(DOMAIN):
if async_entry_is_legacy(entry):

View File

@ -2,18 +2,17 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN
from .coordinator import LinearUpdateCoordinator
from .coordinator import LinearConfigEntry, LinearUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> bool:
"""Set up Linear Garage Door from a config entry."""
ir.async_create_issue(
@ -35,21 +34,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_remove_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> None:
"""Remove a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)

View File

@ -19,6 +19,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type LinearConfigEntry = ConfigEntry[LinearUpdateCoordinator]
@dataclass
class LinearDevice:
@ -32,9 +34,9 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, LinearDevice]]):
"""DataUpdateCoordinator for Linear."""
_devices: list[dict[str, Any]] | None = None
config_entry: ConfigEntry
config_entry: LinearConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, config_entry: LinearConfigEntry) -> None:
"""Initialize DataUpdateCoordinator for Linear."""
super().__init__(
hass,

View File

@ -8,12 +8,10 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import LinearUpdateCoordinator
from .coordinator import LinearConfigEntry
from .entity import LinearEntity
SUPPORTED_SUBDEVICES = ["GDO"]
@ -23,11 +21,11 @@ SCAN_INTERVAL = timedelta(seconds=10)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LinearConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Linear Garage Door cover."""
coordinator: LinearUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities(
LinearCoverEntity(coordinator, device_id, device_data.name, sub_device_id)

View File

@ -6,21 +6,19 @@ from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import LinearUpdateCoordinator
from .coordinator import LinearConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_EMAIL}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: LinearConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: LinearUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),

View File

@ -5,12 +5,10 @@ from typing import Any
from linear_garage_door import Linear
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import LinearUpdateCoordinator
from .coordinator import LinearConfigEntry
from .entity import LinearEntity
SUPPORTED_SUBDEVICES = ["Light"]
@ -18,11 +16,11 @@ SUPPORTED_SUBDEVICES = ["Light"]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LinearConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Linear Garage Door cover."""
coordinator: LinearUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
data = coordinator.data
async_add_entities(

View File

@ -8,19 +8,18 @@ from aiohttp import ClientConnectorError
from livisi.aiolivisi import AioLivisi
from homeassistant import core
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, device_registry as dr
from .const import DOMAIN
from .coordinator import LivisiDataUpdateCoordinator
from .coordinator import LivisiConfigEntry, LivisiDataUpdateCoordinator
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: core.HomeAssistant, entry: LivisiConfigEntry) -> bool:
"""Set up Livisi Smart Home from a config entry."""
web_session = aiohttp_client.async_get_clientsession(hass)
aiolivisi = AioLivisi(web_session)
@ -31,7 +30,7 @@ async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> boo
except ClientConnectorError as exception:
raise ConfigEntryNotReady from exception
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@ -45,16 +44,10 @@ async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> boo
entry.async_create_background_task(
hass, coordinator.ws_connect(), "livisi-ws_connect"
)
entry.async_on_unload(coordinator.websocket.disconnect)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: LivisiConfigEntry) -> bool:
"""Unload a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
unload_success = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await coordinator.websocket.disconnect()
if unload_success:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_success
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -8,23 +8,22 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, LIVISI_STATE_CHANGE, LOGGER, WDS_DEVICE_TYPE
from .coordinator import LivisiDataUpdateCoordinator
from .const import LIVISI_STATE_CHANGE, LOGGER, WDS_DEVICE_TYPE
from .coordinator import LivisiConfigEntry, LivisiDataUpdateCoordinator
from .entity import LivisiEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary_sensor device."""
coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
known_devices = set()
@callback
@ -53,7 +52,7 @@ class LivisiBinarySensor(LivisiEntity, BinarySensorEntity):
def __init__(
self,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
device: dict[str, Any],
capability_name: str,
@ -86,7 +85,7 @@ class LivisiWindowDoorSensor(LivisiBinarySensor):
def __init__(
self,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
device: dict[str, Any],
) -> None:

View File

@ -11,7 +11,6 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@ -19,24 +18,23 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DOMAIN,
LIVISI_STATE_CHANGE,
LOGGER,
MAX_TEMPERATURE,
MIN_TEMPERATURE,
VRCC_DEVICE_TYPE,
)
from .coordinator import LivisiDataUpdateCoordinator
from .coordinator import LivisiConfigEntry, LivisiDataUpdateCoordinator
from .entity import LivisiEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up climate device."""
coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
@callback
def handle_coordinator_update() -> None:
@ -71,7 +69,7 @@ class LivisiClimate(LivisiEntity, ClimateEntity):
def __init__(
self,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
device: dict[str, Any],
) -> None:

View File

@ -26,14 +26,16 @@ from .const import (
LOGGER,
)
type LivisiConfigEntry = ConfigEntry[LivisiDataUpdateCoordinator]
class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
"""Class to manage fetching LIVISI data API."""
config_entry: ConfigEntry
config_entry: LivisiConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, aiolivisi: AioLivisi
self, hass: HomeAssistant, config_entry: LivisiConfigEntry, aiolivisi: AioLivisi
) -> None:
"""Initialize my coordinator."""
super().__init__(

View File

@ -7,14 +7,13 @@ from typing import Any
from livisi.const import CAPABILITY_MAP
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LIVISI_REACHABILITY_CHANGE
from .coordinator import LivisiDataUpdateCoordinator
from .coordinator import LivisiConfigEntry, LivisiDataUpdateCoordinator
class LivisiEntity(CoordinatorEntity[LivisiDataUpdateCoordinator]):
@ -24,7 +23,7 @@ class LivisiEntity(CoordinatorEntity[LivisiDataUpdateCoordinator]):
def __init__(
self,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
device: dict[str, Any],
*,

View File

@ -5,24 +5,23 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, LIVISI_STATE_CHANGE, LOGGER, SWITCH_DEVICE_TYPES
from .coordinator import LivisiDataUpdateCoordinator
from .const import LIVISI_STATE_CHANGE, LOGGER, SWITCH_DEVICE_TYPES
from .coordinator import LivisiConfigEntry, LivisiDataUpdateCoordinator
from .entity import LivisiEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up switch device."""
coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
@callback
def handle_coordinator_update() -> None:
@ -52,7 +51,7 @@ class LivisiSwitch(LivisiEntity, SwitchEntity):
def __init__(
self,
config_entry: ConfigEntry,
config_entry: LivisiConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
device: dict[str, Any],
) -> None:

View File

@ -93,6 +93,9 @@
"battery_time_to_full_charge": {
"default": "mdi:battery-clock"
},
"esa_opt_out_state": {
"default": "mdi:home-lightning-bolt"
},
"evse_state": {
"default": "mdi:ev-station"
},

View File

@ -93,6 +93,13 @@ CHARGE_STATE_MAP = {
clusters.PowerSource.Enums.BatChargeStateEnum.kUnknownEnumValue: None,
}
DEM_OPT_OUT_STATE_MAP = {
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut: "no_opt_out",
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut: "local_opt_out",
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kGridOptOut: "grid_opt_out",
clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut: "opt_out",
}
ESA_STATE_MAP = {
clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOffline: "offline",
clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline: "online",
@ -1159,6 +1166,19 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterSensor,
required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ESAOptOutState",
translation_key="esa_opt_out_state",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=list(DEM_OPT_OUT_STATE_MAP.values()),
measurement_to_ha=DEM_OPT_OUT_STATE_MAP.get,
),
entity_class=MatterSensor,
required_attributes=(clusters.DeviceEnergyManagement.Attributes.OptOutState,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(

View File

@ -363,6 +363,15 @@
"paused": "[%key:common::state::paused%]"
}
},
"esa_opt_out_state": {
"name": "Energy optimization opt-out",
"state": {
"no_opt_out": "[%key:common::state::off%]",
"local_opt_out": "Local",
"grid_opt_out": "Grid",
"opt_out": "Local and grid"
}
},
"evse_fault_state": {
"name": "Fault state",
"state": {

View File

@ -27,7 +27,12 @@ from . import (
MediaPlayerDeviceClass,
SearchMedia,
)
from .const import MediaPlayerEntityFeature, MediaPlayerState
from .const import (
ATTR_MEDIA_FILTER_CLASSES,
MediaClass,
MediaPlayerEntityFeature,
MediaPlayerState,
)
INTENT_MEDIA_PAUSE = "HassMediaPause"
INTENT_MEDIA_UNPAUSE = "HassMediaUnpause"
@ -231,6 +236,7 @@ class MediaSearchAndPlayHandler(intent.IntentHandler):
intent_type = INTENT_MEDIA_SEARCH_AND_PLAY
slot_schema = {
vol.Required("search_query"): cv.string,
vol.Optional("media_class"): vol.In([cls.value for cls in MediaClass]),
# Optional name/area/floor slots handled by intent matcher
vol.Optional("name"): cv.string,
vol.Optional("area"): cv.string,
@ -285,14 +291,23 @@ class MediaSearchAndPlayHandler(intent.IntentHandler):
target_entity = match_result.states[0]
target_entity_id = target_entity.entity_id
# Get media class if provided
media_class_slot = slots.get("media_class", {})
media_class_value = media_class_slot.get("value")
# Build search service data
search_data = {"search_query": search_query}
# Add media_filter_classes if media_class is provided
if media_class_value:
search_data[ATTR_MEDIA_FILTER_CLASSES] = [media_class_value]
# 1. Search Media
try:
search_response = await hass.services.async_call(
DOMAIN,
SERVICE_SEARCH_MEDIA,
{
"search_query": search_query,
},
search_data,
target={
"entity_id": target_entity_id,
},

View File

@ -1294,3 +1294,30 @@ STATE_PROGRAM_ID: dict[int, dict[int, str]] = {
MieleAppliance.ROBOT_VACUUM_CLEANER: ROBOT_VACUUM_CLEANER_PROGRAM_ID,
MieleAppliance.COFFEE_SYSTEM: COFFEE_SYSTEM_PROGRAM_ID,
}
class PlatePowerStep(MieleEnum):
"""Plate power settings."""
plate_step_0 = 0
plate_step_warming = 110, 220
plate_step_1 = 1
plate_step_2 = 2
plate_step_3 = 3
plate_step_4 = 4
plate_step_5 = 5
plate_step_6 = 6
plate_step_7 = 7
plate_step_8 = 8
plate_step_9 = 9
plate_step_10 = 10
plate_step_11 = 11
plate_step_12 = 12
plate_step_13 = 13
plate_step_14 = 4
plate_step_15 = 15
plate_step_16 = 16
plate_step_17 = 17
plate_step_18 = 18
plate_step_boost = 117, 118, 218
missing2none = -9999

View File

@ -56,30 +56,27 @@
"plate": {
"default": "mdi:circle-outline",
"state": {
"0": "mdi:circle-outline",
"110": "mdi:alpha-w-circle-outline",
"220": "mdi:alpha-w-circle-outline",
"1": "mdi:circle-slice-1",
"2": "mdi:circle-slice-1",
"3": "mdi:circle-slice-2",
"4": "mdi:circle-slice-2",
"5": "mdi:circle-slice-3",
"6": "mdi:circle-slice-3",
"7": "mdi:circle-slice-4",
"8": "mdi:circle-slice-4",
"9": "mdi:circle-slice-5",
"10": "mdi:circle-slice-5",
"11": "mdi:circle-slice-5",
"12": "mdi:circle-slice-6",
"13": "mdi:circle-slice-6",
"14": "mdi:circle-slice-6",
"15": "mdi:circle-slice-7",
"16": "mdi:circle-slice-7",
"17": "mdi:circle-slice-8",
"18": "mdi:circle-slice-8",
"117": "mdi:alpha-b-circle-outline",
"118": "mdi:alpha-b-circle-outline",
"217": "mdi:alpha-b-circle-outline"
"plate_step_0": "mdi:circle-outline",
"plate_step_warming": "mdi:alpha-w-circle-outline",
"plate_step_1": "mdi:circle-slice-1",
"plate_step_2": "mdi:circle-slice-1",
"plate_step_3": "mdi:circle-slice-2",
"plate_step_4": "mdi:circle-slice-2",
"plate_step_5": "mdi:circle-slice-3",
"plate_step_6": "mdi:circle-slice-3",
"plate_step_7": "mdi:circle-slice-4",
"plate_step_8": "mdi:circle-slice-4",
"plate_step_9": "mdi:circle-slice-5",
"plate_step_10": "mdi:circle-slice-5",
"plate_step_11": "mdi:circle-slice-5",
"plate_step_12": "mdi:circle-slice-6",
"plate_step_13": "mdi:circle-slice-6",
"plate_step_14": "mdi:circle-slice-6",
"plate_step_15": "mdi:circle-slice-7",
"plate_step_16": "mdi:circle-slice-7",
"plate_step_17": "mdi:circle-slice-8",
"plate_step_18": "mdi:circle-slice-8",
"plate_step_boost": "mdi:alpha-b-circle-outline"
}
},
"program_type": {

View File

@ -33,6 +33,7 @@ from .const import (
STATE_PROGRAM_PHASE,
STATE_STATUS_TAGS,
MieleAppliance,
PlatePowerStep,
StateDryingStep,
StateProgramType,
StateStatus,
@ -46,34 +47,6 @@ _LOGGER = logging.getLogger(__name__)
DISABLED_TEMPERATURE = -32768
PLATE_POWERS = [
"0",
"110",
"220",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"117",
"118",
"217",
]
DEFAULT_PLATE_COUNT = 4
PLATE_COUNT = {
@ -543,8 +516,8 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
translation_placeholders={"plate_no": str(i)},
zone=i,
device_class=SensorDeviceClass.ENUM,
options=PLATE_POWERS,
value_fn=lambda value: value.state_plate_step[0].value_raw,
options=sorted(PlatePowerStep.keys()),
value_fn=lambda value: None,
),
)
for i in range(1, 7)
@ -683,12 +656,19 @@ class MielePlateSensor(MieleSensor):
def native_value(self) -> StateType:
"""Return the state of the plate sensor."""
# state_plate_step is [] if all zones are off
plate_power = (
self.device.state_plate_step[self.entity_description.zone - 1].value_raw
if self.device.state_plate_step
else 0
return (
PlatePowerStep(
cast(
int,
self.device.state_plate_step[
self.entity_description.zone - 1
].value_raw,
)
).name
if self.device.state_plate_step
else PlatePowerStep.plate_step_0
)
return str(plate_power)
class MieleStatusSensor(MieleSensor):

View File

@ -203,30 +203,27 @@
"plate": {
"name": "Plate {plate_no}",
"state": {
"0": "0",
"110": "Warming",
"220": "[%key:component::miele::entity::sensor::plate::state::110%]",
"1": "1",
"2": "1\u2022",
"3": "2",
"4": "2\u2022",
"5": "3",
"6": "3\u2022",
"7": "4",
"8": "4\u2022",
"9": "5",
"10": "5\u2022",
"11": "6",
"12": "6\u2022",
"13": "7",
"14": "7\u2022",
"15": "8",
"16": "8\u2022",
"17": "9",
"18": "9\u2022",
"117": "Boost",
"118": "[%key:component::miele::entity::sensor::plate::state::117%]",
"217": "[%key:component::miele::entity::sensor::plate::state::117%]"
"power_step_0": "0",
"power_step_warm": "Warming",
"power_step_1": "1",
"power_step_2": "1\u2022",
"power_step_3": "2",
"power_step_4": "2\u2022",
"power_step_5": "3",
"power_step_6": "3\u2022",
"power_step_7": "4",
"power_step_8": "4\u2022",
"power_step_9": "5",
"power_step_10": "5\u2022",
"power_step_11": "6",
"power_step_12": "6\u2022",
"power_step_13": "7",
"power_step_14": "7\u2022",
"power_step_15": "8",
"power_step_16": "8\u2022",
"power_step_17": "9",
"power_step_18": "9\u2022",
"power_step_boost": "Boost"
}
},
"drying_step": {

View File

@ -3,7 +3,8 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from music_assistant_client import MusicAssistantClient
@ -31,7 +32,7 @@ if TYPE_CHECKING:
from homeassistant.helpers.typing import ConfigType
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
CONNECT_TIMEOUT = 10
LISTEN_READY_TIMEOUT = 30
@ -39,6 +40,7 @@ LISTEN_READY_TIMEOUT = 30
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
type PlayerAddCallback = Callable[[str], None]
@dataclass
@ -47,6 +49,8 @@ class MusicAssistantEntryData:
mass: MusicAssistantClient
listen_task: asyncio.Task
discovered_players: set[str] = field(default_factory=set)
platform_handlers: dict[Platform, PlayerAddCallback] = field(default_factory=dict)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@ -122,6 +126,33 @@ async def async_setup_entry(
# initialize platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# register listener for new players
async def handle_player_added(event: MassEvent) -> None:
"""Handle Mass Player Added event."""
if TYPE_CHECKING:
assert event.object_id is not None
if event.object_id in entry.runtime_data.discovered_players:
return
player = mass.players.get(event.object_id)
if TYPE_CHECKING:
assert player is not None
if not player.expose_to_ha:
return
entry.runtime_data.discovered_players.add(event.object_id)
# run callback for each platform
for callback in entry.runtime_data.platform_handlers.values():
callback(event.object_id)
entry.async_on_unload(mass.subscribe(handle_player_added, EventType.PLAYER_ADDED))
# add all current players
for player in mass.players:
if not player.expose_to_ha:
continue
entry.runtime_data.discovered_players.add(player.player_id)
for callback in entry.runtime_data.platform_handlers.values():
callback(player.player_id)
# register listener for removed players
async def handle_player_removed(event: MassEvent) -> None:
"""Handle Mass Player Removed event."""

View File

@ -0,0 +1,53 @@
"""Music Assistant Button platform."""
from __future__ import annotations
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import MusicAssistantConfigEntry
from .entity import MusicAssistantEntity
from .helpers import catch_musicassistant_error
async def async_setup_entry(
hass: HomeAssistant,
entry: MusicAssistantConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Music Assistant MediaPlayer(s) from Config Entry."""
mass = entry.runtime_data.mass
def add_player(player_id: str) -> None:
"""Handle add player."""
async_add_entities(
[
# Add button entity to favorite the currently playing item on the player
MusicAssistantFavoriteButton(mass, player_id)
]
)
# register callback to add players when they are discovered
entry.runtime_data.platform_handlers.setdefault(Platform.BUTTON, add_player)
class MusicAssistantFavoriteButton(MusicAssistantEntity, ButtonEntity):
"""Representation of a Button entity to favorite the currently playing item on a player."""
entity_description = ButtonEntityDescription(
key="favorite_now_playing",
translation_key="favorite_now_playing",
)
@property
def available(self) -> bool:
"""Return availability of entity."""
# mark the button as unavailable if the player has no current media item
return super().available and self.player.current_media is not None
@catch_musicassistant_error
async def async_press(self) -> None:
"""Handle the button press command."""
await self.mass.players.add_currently_playing_to_favorites(self.player_id)

View File

@ -0,0 +1,28 @@
"""Helpers for the Music Assistant integration."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
import functools
from typing import Any
from music_assistant_models.errors import MusicAssistantError
from homeassistant.exceptions import HomeAssistantError
def catch_musicassistant_error[**_P, _R](
func: Callable[_P, Coroutine[Any, Any, _R]],
) -> Callable[_P, Coroutine[Any, Any, _R]]:
"""Check and convert commands to players."""
@functools.wraps(func)
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
"""Catch Music Assistant errors and convert to Home Assistant error."""
try:
return await func(*args, **kwargs)
except MusicAssistantError as err:
error_msg = str(err) or err.__class__.__name__
raise HomeAssistantError(error_msg) from err
return wrapper

View File

@ -1,4 +1,11 @@
{
"entity": {
"button": {
"favorite_now_playing": {
"default": "mdi:heart-plus"
}
}
},
"services": {
"play_media": { "service": "mdi:play" },
"play_announcement": { "service": "mdi:bullhorn" },

View File

@ -3,11 +3,10 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine, Mapping
from collections.abc import Mapping
from contextlib import suppress
import functools
import os
from typing import TYPE_CHECKING, Any, Concatenate
from typing import TYPE_CHECKING, Any
from music_assistant_models.constants import PLAYER_CONTROL_NONE
from music_assistant_models.enums import (
@ -18,7 +17,7 @@ from music_assistant_models.enums import (
QueueOption,
RepeatMode as MassRepeatMode,
)
from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
from music_assistant_models.errors import MediaNotFoundError
from music_assistant_models.event import MassEvent
from music_assistant_models.media_items import ItemMapping, MediaItemType, Track
from music_assistant_models.player_queue import PlayerQueue
@ -40,7 +39,7 @@ from homeassistant.components.media_player import (
SearchMediaQuery,
async_process_play_media_url,
)
from homeassistant.const import ATTR_NAME, STATE_OFF
from homeassistant.const import ATTR_NAME, STATE_OFF, Platform
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_registry as er
@ -76,6 +75,7 @@ from .const import (
DOMAIN,
)
from .entity import MusicAssistantEntity
from .helpers import catch_musicassistant_error
from .media_browser import async_browse_media, async_search_media
from .schemas import QUEUE_DETAILS_SCHEMA, queue_item_dict_from_mass_item
@ -120,25 +120,6 @@ SERVICE_TRANSFER_QUEUE = "transfer_queue"
SERVICE_GET_QUEUE = "get_queue"
def catch_musicassistant_error[_R, **P](
func: Callable[Concatenate[MusicAssistantPlayer, P], Coroutine[Any, Any, _R]],
) -> Callable[Concatenate[MusicAssistantPlayer, P], Coroutine[Any, Any, _R]]:
"""Check and log commands to players."""
@functools.wraps(func)
async def wrapper(
self: MusicAssistantPlayer, *args: P.args, **kwargs: P.kwargs
) -> _R:
"""Catch Music Assistant errors and convert to Home Assistant error."""
try:
return await func(self, *args, **kwargs)
except MusicAssistantError as err:
error_msg = str(err) or err.__class__.__name__
raise HomeAssistantError(error_msg) from err
return wrapper
async def async_setup_entry(
hass: HomeAssistant,
entry: MusicAssistantConfigEntry,
@ -146,33 +127,13 @@ async def async_setup_entry(
) -> None:
"""Set up Music Assistant MediaPlayer(s) from Config Entry."""
mass = entry.runtime_data.mass
added_ids = set()
async def handle_player_added(event: MassEvent) -> None:
"""Handle Mass Player Added event."""
if TYPE_CHECKING:
assert event.object_id is not None
if event.object_id in added_ids:
return
player = mass.players.get(event.object_id)
if TYPE_CHECKING:
assert player is not None
if not player.expose_to_ha:
return
added_ids.add(event.object_id)
async_add_entities([MusicAssistantPlayer(mass, event.object_id)])
def add_player(player_id: str) -> None:
"""Handle add player."""
async_add_entities([MusicAssistantPlayer(mass, player_id)])
# register listener for new players
entry.async_on_unload(mass.subscribe(handle_player_added, EventType.PLAYER_ADDED))
mass_players = []
# add all current players
for player in mass.players:
if not player.expose_to_ha:
continue
added_ids.add(player.player_id)
mass_players.append(MusicAssistantPlayer(mass, player.player_id))
async_add_entities(mass_players)
# register callback to add players when they are discovered
entry.runtime_data.platform_handlers.setdefault(Platform.MEDIA_PLAYER, add_player)
# add platform service for play_media with advanced options
platform = async_get_current_platform()

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