Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion homeassistant/components/fritz/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"quality_scale": "bronze",
"requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==1.0.2"],
"requirements": ["fritzconnection[qr]==1.15.1", "xmltodict==1.0.2"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"requirements": ["fritzconnection[qr]==1.15.0"]
"requirements": ["fritzconnection[qr]==1.15.1"]
}
6 changes: 3 additions & 3 deletions homeassistant/components/input_number/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@
},
"services": {
"decrement": {
"description": "Decrements the current value by 1 step.",
"description": "Decrements the value of an input number by 1 step.",
"name": "Decrement"
},
"increment": {
"description": "Increments the current value by 1 step.",
"description": "Increments the value of an input number by 1 step.",
"name": "Increment"
},
"reload": {
"description": "Reloads helpers from the YAML-configuration.",
"name": "[%key:common::action::reload%]"
},
"set_value": {
"description": "Sets the value.",
"description": "Sets the value of an input number.",
"fields": {
"value": {
"description": "The target value.",
Expand Down
20 changes: 16 additions & 4 deletions homeassistant/components/intent_script/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from homeassistant.components.script import CONF_MODE
from homeassistant.const import CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
intent,
Expand All @@ -18,6 +19,7 @@
template,
)
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.script import async_validate_actions_config
from homeassistant.helpers.typing import ConfigType

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -85,19 +87,29 @@ async def async_reload(hass: HomeAssistant, service_call: ServiceCall) -> None:

new_intents = new_config[DOMAIN]

async_load_intents(hass, new_intents)
await async_load_intents(hass, new_intents)


def async_load_intents(hass: HomeAssistant, intents: dict[str, ConfigType]) -> None:
async def async_load_intents(
hass: HomeAssistant, intents: dict[str, ConfigType]
) -> None:
"""Load YAML intents into the intent system."""
hass.data[DOMAIN] = intents

for intent_type, conf in intents.items():
if CONF_ACTION in conf:
try:
actions = await async_validate_actions_config(hass, conf[CONF_ACTION])
except (vol.Invalid, HomeAssistantError) as exc:
_LOGGER.error(
"Failed to validate actions for intent %s: %s", intent_type, exc
)
continue # Skip this intent

script_mode: str = conf.get(CONF_MODE, script.DEFAULT_SCRIPT_MODE)
conf[CONF_ACTION] = script.Script(
hass,
conf[CONF_ACTION],
actions,
f"Intent Script {intent_type}",
DOMAIN,
script_mode=script_mode,
Expand All @@ -109,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the intent script component."""
intents = config[DOMAIN]

async_load_intents(hass, intents)
await async_load_intents(hass, intents)

async def _handle_reload(service_call: ServiceCall) -> None:
return await async_reload(hass, service_call)
Expand Down
30 changes: 30 additions & 0 deletions homeassistant/components/min_max/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
Expand All @@ -25,7 +26,9 @@
STATE_UNKNOWN,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
Expand Down Expand Up @@ -259,6 +262,7 @@ async def async_added_to_hass(self) -> None:
)
self._async_min_max_sensor_state_listener(state_event, update_state=False)

self._update_device_class()
self._calc_values()

@property
Expand Down Expand Up @@ -345,6 +349,32 @@ def _async_min_max_sensor_state_listener(
self._calc_values()
self.async_write_ha_state()

@callback
def _update_device_class(self) -> None:
"""Update device_class based on source entities.

If all source entities have the same device_class, inherit it.
Otherwise, leave device_class as None.
"""
device_classes: list[SensorDeviceClass | None] = []

for entity_id in self._entity_ids:
try:
device_class = get_device_class(self.hass, entity_id)
if device_class:
device_classes.append(SensorDeviceClass(device_class))
else:
device_classes.append(None)
except (HomeAssistantError, ValueError):
# If we can't get device class for any entity, don't set it
device_classes.append(None)

# Only inherit device_class if all entities have the same non-None device_class
if device_classes and all(
dc is not None and dc == device_classes[0] for dc in device_classes
):
self._attr_device_class = device_classes[0]

@callback
def _calc_values(self) -> None:
"""Calculate the values."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/nibe_heatpump/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["nibe==2.21.0"]
"requirements": ["nibe==2.22.0"]
}
8 changes: 7 additions & 1 deletion homeassistant/components/number/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,8 @@ class NumberDeviceClass(StrEnum):
}

# We translate units that were using using the legacy coding of μ \u00b5
# to units using recommended coding of μ \u03bc
# to units using recommended coding of μ \u03bc and
# we convert alternative accepted units to the preferred unit.
AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
Expand All @@ -604,4 +605,9 @@ class NumberDeviceClass(StrEnum):
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
"mVAr": UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE,
"VAr": UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
"kVAr": UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE,
"VArh": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR,
"kVArh": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR,
}
11 changes: 11 additions & 0 deletions homeassistant/components/portainer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN
from .coordinator import PortainerCoordinator
from .services import async_setup_services

_PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Expand All @@ -25,6 +29,7 @@
Platform.BUTTON,
]

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

type PortainerConfigEntry = ConfigEntry[PortainerCoordinator]

Expand All @@ -49,6 +54,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) ->
return True


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Portainer integration."""
await async_setup_services(hass)
return True


async def async_unload_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/portainer/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
DOMAIN = "portainer"
DEFAULT_NAME = "Portainer"


ENDPOINT_STATUS_DOWN = 2

CONTAINER_STATE_RUNNING = "running"
5 changes: 5 additions & 0 deletions homeassistant/components/portainer/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,10 @@
}
}
}
},
"services": {
"prune_images": {
"service": "mdi:delete-sweep"
}
}
}
10 changes: 2 additions & 8 deletions homeassistant/components/portainer/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ rules:
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
Expand All @@ -33,10 +30,7 @@ rules:
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates:
status: exempt
comment: |
No explicit parallel updates are defined.
parallel-updates: todo
reauthentication-flow:
status: todo
comment: |
Expand Down
115 changes: 115 additions & 0 deletions homeassistant/components/portainer/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Services for the Portainer integration."""

from datetime import timedelta

from pyportainer import (
PortainerAuthenticationError,
PortainerConnectionError,
PortainerTimeoutError,
)
import voluptuous as vol

from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.service import async_extract_config_entry_ids

from .const import DOMAIN
from .coordinator import PortainerConfigEntry

ATTR_DATE_UNTIL = "until"
ATTR_DANGLING = "dangling"

SERVICE_PRUNE_IMAGES = "prune_images"
SERVICE_PRUNE_IMAGES_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_DATE_UNTIL): vol.All(
cv.time_period, vol.Range(min=timedelta(minutes=1))
),
vol.Optional(ATTR_DANGLING): cv.boolean,
},
)


async def _extract_config_entry(service_call: ServiceCall) -> PortainerConfigEntry:
"""Extract config entry from the service call."""
target_entry_ids = await async_extract_config_entry_ids(service_call)
target_entries: list[PortainerConfigEntry] = [
loaded_entry
for loaded_entry in service_call.hass.config_entries.async_loaded_entries(
DOMAIN
)
if loaded_entry.entry_id in target_entry_ids
]
if not target_entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
return target_entries[0]


async def _get_endpoint_id(
call: ServiceCall,
config_entry: PortainerConfigEntry,
) -> int:
"""Get endpoint data from device ID."""
device_reg = dr.async_get(call.hass)
device_id = call.data[ATTR_DEVICE_ID]
device = device_reg.async_get(device_id)
assert device
coordinator = config_entry.runtime_data

endpoint_data = None
for data in coordinator.data.values():
if (
DOMAIN,
f"{config_entry.entry_id}_{data.endpoint.id}",
) in device.identifiers:
endpoint_data = data
break

assert endpoint_data
return endpoint_data.endpoint.id


async def prune_images(call: ServiceCall) -> None:
"""Prune unused images in Portainer, with more controls."""
config_entry = await _extract_config_entry(call)
coordinator = config_entry.runtime_data
endpoint_id = await _get_endpoint_id(call, config_entry)

try:
await coordinator.portainer.images_prune(
endpoint_id=endpoint_id,
until=call.data.get(ATTR_DATE_UNTIL),
dangling=call.data.get(ATTR_DANGLING, False),
)
except PortainerAuthenticationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth_no_details",
) from err
except PortainerConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_no_details",
) from err
except PortainerTimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_connect_no_details",
) from err


async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""

hass.services.async_register(
DOMAIN,
SERVICE_PRUNE_IMAGES,
prune_images,
SERVICE_PRUNE_IMAGES_SCHEMA,
)
Loading
Loading