mirror of
https://github.com/home-assistant/core.git
synced 2025-10-08 03:09:27 +00:00
Compare commits
1 Commits
trigger_ac
...
mqtt-suben
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96c111c96c |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1065,8 +1065,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/nilu/ @hfurubotten
|
||||
/homeassistant/components/nina/ @DeerMaximum
|
||||
/tests/components/nina/ @DeerMaximum
|
||||
/homeassistant/components/nintendo_parental/ @pantherale0
|
||||
/tests/components/nintendo_parental/ @pantherale0
|
||||
/homeassistant/components/nissan_leaf/ @filcole
|
||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||
|
@@ -458,6 +458,7 @@ SUBENTRY_PLATFORMS = [
|
||||
Platform.LOCK,
|
||||
Platform.NOTIFY,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
@@ -1141,6 +1142,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
|
||||
Platform.LOCK.value: None,
|
||||
Platform.NOTIFY.value: None,
|
||||
Platform.NUMBER.value: validate_number_platform_config,
|
||||
Platform.SELECT: None,
|
||||
Platform.SENSOR.value: validate_sensor_platform_config,
|
||||
Platform.SWITCH.value: None,
|
||||
}
|
||||
@@ -1367,6 +1369,7 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
|
||||
custom_filtering=True,
|
||||
),
|
||||
},
|
||||
Platform.SELECT.value: {},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_DEVICE_CLASS: PlatformField(
|
||||
selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False
|
||||
@@ -3103,6 +3106,34 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
|
||||
),
|
||||
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
|
||||
},
|
||||
Platform.SELECT.value: {
|
||||
CONF_COMMAND_TOPIC: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
required=True,
|
||||
validator=valid_publish_topic,
|
||||
error="invalid_publish_topic",
|
||||
),
|
||||
CONF_COMMAND_TEMPLATE: PlatformField(
|
||||
selector=TEMPLATE_SELECTOR,
|
||||
required=False,
|
||||
validator=validate(cv.template),
|
||||
error="invalid_template",
|
||||
),
|
||||
CONF_STATE_TOPIC: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
required=False,
|
||||
validator=valid_subscribe_topic,
|
||||
error="invalid_subscribe_topic",
|
||||
),
|
||||
CONF_VALUE_TEMPLATE: PlatformField(
|
||||
selector=TEMPLATE_SELECTOR,
|
||||
required=False,
|
||||
validator=validate(cv.template),
|
||||
error="invalid_template",
|
||||
),
|
||||
CONF_OPTIONS: PlatformField(selector=OPTIONS_SELECTOR, required=True),
|
||||
CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
|
||||
},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_STATE_TOPIC: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
|
@@ -346,6 +346,7 @@
|
||||
"mode_state_template": "Operation mode value template",
|
||||
"on_command_type": "ON command type",
|
||||
"optimistic": "Optimistic",
|
||||
"options": "Set options",
|
||||
"payload_off": "Payload \"off\"",
|
||||
"payload_on": "Payload \"on\"",
|
||||
"payload_press": "Payload \"press\"",
|
||||
@@ -393,6 +394,7 @@
|
||||
"mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the operation mode state. [Learn more.]({url}#mode_state_template)",
|
||||
"on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.",
|
||||
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
|
||||
"options": "List of options that can be selected.",
|
||||
"payload_off": "The payload that represents the \"off\" state.",
|
||||
"payload_on": "The payload that represents the \"on\" state.",
|
||||
"payload_press": "The payload to send when the button is triggered.",
|
||||
@@ -1334,6 +1336,7 @@
|
||||
"lock": "[%key:component::lock::title%]",
|
||||
"notify": "[%key:component::notify::title%]",
|
||||
"number": "[%key:component::number::title%]",
|
||||
"select": "[%key:component::select::title%]",
|
||||
"sensor": "[%key:component::sensor::title%]",
|
||||
"switch": "[%key:component::switch::title%]"
|
||||
}
|
||||
|
@@ -1,51 +0,0 @@
|
||||
"""The Nintendo Switch Parental Controls integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pynintendoparental import Authenticator
|
||||
from pynintendoparental.exceptions import (
|
||||
InvalidOAuthConfigurationException,
|
||||
InvalidSessionTokenException,
|
||||
)
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SESSION_TOKEN, DOMAIN
|
||||
from .coordinator import NintendoParentalConfigEntry, NintendoUpdateCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: NintendoParentalConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Nintendo Switch Parental Controls from a config entry."""
|
||||
try:
|
||||
nintendo_auth = await Authenticator.complete_login(
|
||||
auth=None,
|
||||
response_token=entry.data[CONF_SESSION_TOKEN],
|
||||
is_session_token=True,
|
||||
client_session=async_get_clientsession(hass),
|
||||
)
|
||||
except (InvalidSessionTokenException, InvalidOAuthConfigurationException) as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_expired",
|
||||
) from err
|
||||
entry.runtime_data = coordinator = NintendoUpdateCoordinator(
|
||||
hass, nintendo_auth, entry
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: NintendoParentalConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
@@ -1,61 +0,0 @@
|
||||
"""Config flow for the Nintendo Switch Parental Controls integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pynintendoparental import Authenticator
|
||||
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SESSION_TOKEN, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Nintendo Switch Parental Controls."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a new config flow instance."""
|
||||
self.auth: Authenticator | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if self.auth is None:
|
||||
self.auth = Authenticator.generate_login(
|
||||
client_session=async_get_clientsession(self.hass)
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self.auth.complete_login(
|
||||
self.auth, user_input[CONF_API_TOKEN], False
|
||||
)
|
||||
except (ValueError, InvalidSessionTokenException, HttpException):
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
assert self.auth.account_id
|
||||
await self.async_set_unique_id(self.auth.account_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self.auth.account_id,
|
||||
data={
|
||||
CONF_SESSION_TOKEN: self.auth.get_session_token,
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
description_placeholders={"link": self.auth.login_url},
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
||||
errors=errors,
|
||||
)
|
@@ -1,5 +0,0 @@
|
||||
"""Constants for the Nintendo Switch Parental Controls integration."""
|
||||
|
||||
DOMAIN = "nintendo_parental"
|
||||
CONF_UPDATE_INTERVAL = "update_interval"
|
||||
CONF_SESSION_TOKEN = "session_token"
|
@@ -1,52 +0,0 @@
|
||||
"""Nintendo Parental Controls data coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pynintendoparental import Authenticator, NintendoParental
|
||||
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type NintendoParentalConfigEntry = ConfigEntry[NintendoUpdateCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Nintendo data update coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
authenticator: Authenticator,
|
||||
config_entry: NintendoParentalConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize update coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.api = NintendoParental(
|
||||
authenticator, hass.config.time_zone, hass.config.language
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data from Nintendo's API."""
|
||||
try:
|
||||
return await self.api.update()
|
||||
except InvalidOAuthConfigurationException as err:
|
||||
raise ConfigEntryError(
|
||||
err, translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
@@ -1,41 +0,0 @@
|
||||
"""Base entity definition for Nintendo Parental."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pynintendoparental.device import Device
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NintendoUpdateCoordinator
|
||||
|
||||
|
||||
class NintendoDevice(CoordinatorEntity[NintendoUpdateCoordinator]):
|
||||
"""Represent a Nintendo Switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: NintendoUpdateCoordinator, device: Device, key: str
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_unique_id = f"{device.device_id}_{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.device_id)},
|
||||
manufacturer="Nintendo",
|
||||
name=device.name,
|
||||
sw_version=device.extra["firmwareVersion"]["displayedVersion"],
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is loaded."""
|
||||
await super().async_added_to_hass()
|
||||
self._device.add_device_callback(self.async_write_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""When will be removed from HASS."""
|
||||
self._device.remove_device_callback(self.async_write_ha_state)
|
||||
await super().async_will_remove_from_hass()
|
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "nintendo_parental",
|
||||
"name": "Nintendo Switch Parental Controls",
|
||||
"codeowners": ["@pantherale0"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nintendo_parental",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoparental==1.0.1"]
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
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: |
|
||||
No custom actions are defined.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
No IP discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
No discovery.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
No specific icons defined.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
comment: |
|
||||
No issues in integration
|
||||
status: exempt
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
@@ -1,91 +0,0 @@
|
||||
"""Sensor platform for Nintendo Parental."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import NintendoParentalConfigEntry, NintendoUpdateCoordinator
|
||||
from .entity import Device, NintendoDevice
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class NintendoParentalSensor(StrEnum):
|
||||
"""Store keys for Nintendo Parental sensors."""
|
||||
|
||||
PLAYING_TIME = "playing_time"
|
||||
TIME_REMAINING = "time_remaining"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class NintendoParentalSensorEntityDescription(SensorEntityDescription):
|
||||
"""Description for Nintendo Parental sensor entities."""
|
||||
|
||||
value_fn: Callable[[Device], int | float | None]
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[NintendoParentalSensorEntityDescription, ...] = (
|
||||
NintendoParentalSensorEntityDescription(
|
||||
key=NintendoParentalSensor.PLAYING_TIME,
|
||||
translation_key=NintendoParentalSensor.PLAYING_TIME,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.today_playing_time,
|
||||
),
|
||||
NintendoParentalSensorEntityDescription(
|
||||
key=NintendoParentalSensor.TIME_REMAINING,
|
||||
translation_key=NintendoParentalSensor.TIME_REMAINING,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.today_time_remaining,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: NintendoParentalConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
async_add_devices(
|
||||
NintendoParentalSensorEntity(entry.runtime_data, device, sensor)
|
||||
for device in entry.runtime_data.api.devices.values()
|
||||
for sensor in SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class NintendoParentalSensorEntity(NintendoDevice, SensorEntity):
|
||||
"""Represent a single sensor."""
|
||||
|
||||
entity_description: NintendoParentalSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NintendoUpdateCoordinator,
|
||||
device: Device,
|
||||
description: NintendoParentalSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, device=device, key=description.key)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
"""Return the native value."""
|
||||
return self.entity_description.value_fn(self._device)
|
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To obtain your access token, click [Nintendo Login]({link}) to sign in to your Nintendo account. Then, for the account you want to link, right-click on the red **Select this person** button and choose **Copy Link Address**.",
|
||||
"data": {
|
||||
"api_token": "Access token"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The link copied from the Nintendo website"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"playing_time": {
|
||||
"name": "Used screen time"
|
||||
},
|
||||
"time_remaining": {
|
||||
"name": "Screen time remaining"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_expired": {
|
||||
"message": "Authentication expired. Please remove and re-add the integration to reconnect."
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,7 +22,7 @@
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"auto": "Automatic",
|
||||
"box": "Input field",
|
||||
"box": "Box",
|
||||
"slider": "Slider"
|
||||
}
|
||||
},
|
||||
|
@@ -20,9 +20,8 @@ set_temperature:
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 250
|
||||
max: 100
|
||||
step: 0.5
|
||||
mode: box
|
||||
unit_of_measurement: "°"
|
||||
operation_mode:
|
||||
example: eco
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -21,7 +20,7 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
@@ -455,28 +454,8 @@ async def async_attach_trigger(
|
||||
zwave_js_config = await validate_value_updated_trigger_config(
|
||||
hass, zwave_js_config
|
||||
)
|
||||
|
||||
@callback
|
||||
def run_action(
|
||||
extra_trigger_payload: dict[str, Any],
|
||||
description: str,
|
||||
context: Context | None = None,
|
||||
) -> asyncio.Task[Any]:
|
||||
"""Run action with trigger variables."""
|
||||
|
||||
payload = {
|
||||
"trigger": {
|
||||
**trigger_info["trigger_data"],
|
||||
CONF_PLATFORM: VALUE_UPDATED_PLATFORM_TYPE,
|
||||
"description": description,
|
||||
**extra_trigger_payload,
|
||||
}
|
||||
}
|
||||
|
||||
return hass.async_create_task(action(payload, context))
|
||||
|
||||
return await attach_value_updated_trigger(
|
||||
hass, zwave_js_config[CONF_OPTIONS], run_action
|
||||
hass, zwave_js_config[CONF_OPTIONS], action, trigger_info
|
||||
)
|
||||
|
||||
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
|
||||
|
@@ -17,12 +17,19 @@ from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_PLATFORM,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
TriggerActionType,
|
||||
TriggerConfig,
|
||||
TriggerData,
|
||||
TriggerInfo,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..const import (
|
||||
@@ -120,13 +127,17 @@ _CONFIG_SCHEMA = vol.Schema(
|
||||
class EventTrigger(Trigger):
|
||||
"""Z-Wave JS event trigger."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
_options: dict[str, Any]
|
||||
|
||||
_event_source: str
|
||||
_event_name: str
|
||||
_event_data_filter: dict
|
||||
_job: HassJob
|
||||
_trigger_data: TriggerData
|
||||
_unsubs: list[Callable]
|
||||
_action_runner: TriggerActionRunner
|
||||
|
||||
_platform_type = PLATFORM_TYPE
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
@@ -165,12 +176,14 @@ class EventTrigger(Trigger):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._hass = hass
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
dev_reg = dr.async_get(self._hass)
|
||||
@@ -185,7 +198,8 @@ class EventTrigger(Trigger):
|
||||
self._event_source = options[ATTR_EVENT_SOURCE]
|
||||
self._event_name = options[ATTR_EVENT]
|
||||
self._event_data_filter = options.get(ATTR_EVENT_DATA, {})
|
||||
self._action_runner = run_action
|
||||
self._job = HassJob(action)
|
||||
self._trigger_data = trigger_info["trigger_data"]
|
||||
self._unsubs: list[Callable] = []
|
||||
|
||||
self._create_zwave_listeners()
|
||||
@@ -211,7 +225,9 @@ class EventTrigger(Trigger):
|
||||
if event_data[key] != val:
|
||||
return
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
payload = {
|
||||
**self._trigger_data,
|
||||
CONF_PLATFORM: self._platform_type,
|
||||
ATTR_EVENT_SOURCE: self._event_source,
|
||||
ATTR_EVENT: self._event_name,
|
||||
ATTR_EVENT_DATA: event_data,
|
||||
@@ -221,17 +237,21 @@ class EventTrigger(Trigger):
|
||||
f"Z-Wave JS '{self._event_source}' event '{self._event_name}' was emitted"
|
||||
)
|
||||
|
||||
description = primary_desc
|
||||
if device:
|
||||
device_name = device.name_by_user or device.name
|
||||
payload[ATTR_DEVICE_ID] = device.id
|
||||
home_and_node_id = get_home_and_node_id_from_device_entry(device)
|
||||
assert home_and_node_id
|
||||
payload[ATTR_NODE_ID] = home_and_node_id[1]
|
||||
description = f"{primary_desc} on {device_name}"
|
||||
payload["description"] = f"{primary_desc} on {device_name}"
|
||||
else:
|
||||
payload["description"] = primary_desc
|
||||
|
||||
description = f"{description} with event data: {event_data}"
|
||||
self._action_runner(payload, description)
|
||||
payload["description"] = (
|
||||
f"{payload['description']} with event data: {event_data}"
|
||||
)
|
||||
|
||||
self._hass.async_run_hass_job(self._job, {"trigger": payload})
|
||||
|
||||
@callback
|
||||
def _async_remove(self) -> None:
|
||||
|
@@ -11,12 +11,23 @@ from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.value import Value, get_value_id_str
|
||||
|
||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_OPTIONS, MATCH_ALL
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_PLATFORM,
|
||||
MATCH_ALL,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
TriggerActionType,
|
||||
TriggerConfig,
|
||||
TriggerInfo,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..config_validation import VALUE_SCHEMA
|
||||
@@ -89,7 +100,12 @@ async def async_validate_trigger_config(
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant, options: ConfigType, run_action: TriggerActionRunner
|
||||
hass: HomeAssistant,
|
||||
options: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
*,
|
||||
platform_type: str = PLATFORM_TYPE,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
@@ -105,6 +121,9 @@ async def async_attach_trigger(
|
||||
endpoint = options.get(ATTR_ENDPOINT)
|
||||
property_key = options.get(ATTR_PROPERTY_KEY)
|
||||
unsubs: list[Callable] = []
|
||||
job = HassJob(action)
|
||||
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
|
||||
@callback
|
||||
def async_on_value_updated(
|
||||
@@ -133,8 +152,10 @@ async def async_attach_trigger(
|
||||
return
|
||||
|
||||
device_name = device.name_by_user or device.name
|
||||
description = f"Z-Wave value {value.value_id} updated on {device_name}"
|
||||
|
||||
payload = {
|
||||
**trigger_data,
|
||||
CONF_PLATFORM: platform_type,
|
||||
ATTR_DEVICE_ID: device.id,
|
||||
ATTR_NODE_ID: value.node.node_id,
|
||||
ATTR_COMMAND_CLASS: value.command_class,
|
||||
@@ -148,9 +169,10 @@ async def async_attach_trigger(
|
||||
ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
|
||||
ATTR_CURRENT_VALUE: curr_value,
|
||||
ATTR_CURRENT_VALUE_RAW: curr_value_raw,
|
||||
"description": f"Z-Wave value {value.value_id} updated on {device_name}",
|
||||
}
|
||||
|
||||
run_action(payload, description)
|
||||
hass.async_run_hass_job(job, {"trigger": payload})
|
||||
|
||||
@callback
|
||||
def async_remove() -> None:
|
||||
@@ -201,6 +223,7 @@ async def async_attach_trigger(
|
||||
class ValueUpdatedTrigger(Trigger):
|
||||
"""Z-Wave JS value updated trigger."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
_options: dict[str, Any]
|
||||
|
||||
@classmethod
|
||||
@@ -222,12 +245,16 @@ class ValueUpdatedTrigger(Trigger):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._hass = hass
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
return await async_attach_trigger(self._hass, self._options, run_action)
|
||||
return await async_attach_trigger(
|
||||
self._hass, self._options, action, trigger_info
|
||||
)
|
||||
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -440,7 +440,6 @@ FLOWS = {
|
||||
"nightscout",
|
||||
"niko_home_control",
|
||||
"nina",
|
||||
"nintendo_parental",
|
||||
"nmap_tracker",
|
||||
"nmbs",
|
||||
"nobo_hub",
|
||||
|
@@ -4459,12 +4459,6 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"single_config_entry": true
|
||||
},
|
||||
"nintendo_parental": {
|
||||
"name": "Nintendo Switch Parental Controls",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"nissan_leaf": {
|
||||
"name": "Nissan Leaf",
|
||||
"integration_type": "hub",
|
||||
|
@@ -28,10 +28,8 @@ from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Context,
|
||||
HassJob,
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
get_hassjob_callable_job_type,
|
||||
is_callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||
@@ -180,8 +178,6 @@ _TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||
class Trigger(abc.ABC):
|
||||
"""Trigger class."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
@@ -216,33 +212,14 @@ class Trigger(abc.ABC):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
self._hass = hass
|
||||
|
||||
async def async_attach_action(
|
||||
self,
|
||||
action: TriggerAction,
|
||||
action_payload_builder: TriggerActionPayloadBuilder,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action."""
|
||||
|
||||
@callback
|
||||
def run_action(
|
||||
extra_trigger_payload: dict[str, Any],
|
||||
description: str,
|
||||
context: Context | None = None,
|
||||
) -> asyncio.Task[Any]:
|
||||
"""Run action with trigger variables."""
|
||||
|
||||
payload = action_payload_builder(extra_trigger_payload, description)
|
||||
return self._hass.async_create_task(action(payload, context))
|
||||
|
||||
return await self.async_attach_runner(run_action)
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
"""Attach the trigger."""
|
||||
|
||||
|
||||
class TriggerProtocol(Protocol):
|
||||
@@ -280,43 +257,6 @@ class TriggerConfig:
|
||||
options: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class TriggerActionRunner(Protocol):
|
||||
"""Protocol type for the trigger action runner helper callback."""
|
||||
|
||||
@callback
|
||||
def __call__(
|
||||
self,
|
||||
extra_trigger_payload: dict[str, Any],
|
||||
description: str,
|
||||
context: Context | None = None,
|
||||
) -> asyncio.Task[Any]:
|
||||
"""Define trigger action runner type.
|
||||
|
||||
Returns:
|
||||
A Task that allows awaiting for the action to finish.
|
||||
"""
|
||||
|
||||
|
||||
class TriggerActionPayloadBuilder(Protocol):
|
||||
"""Protocol type for the trigger action payload builder."""
|
||||
|
||||
def __call__(
|
||||
self, extra_trigger_payload: dict[str, Any], description: str
|
||||
) -> dict[str, Any]:
|
||||
"""Define trigger action payload builder type."""
|
||||
|
||||
|
||||
class TriggerAction(Protocol):
|
||||
"""Protocol type for trigger action callback."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
run_variables: dict[str, Any],
|
||||
context: Context | None = None,
|
||||
) -> Any:
|
||||
"""Define action callback type."""
|
||||
|
||||
|
||||
class TriggerActionType(Protocol):
|
||||
"""Protocol type for trigger action callback."""
|
||||
|
||||
@@ -553,71 +493,6 @@ def _trigger_action_wrapper(
|
||||
return wrapper_func
|
||||
|
||||
|
||||
async def _async_attach_trigger_cls(
|
||||
hass: HomeAssistant,
|
||||
trigger_cls: type[Trigger],
|
||||
trigger_key: str,
|
||||
conf: ConfigType,
|
||||
action: Callable,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Initialize a new Trigger class and attach it."""
|
||||
|
||||
def action_payload_builder(
|
||||
extra_trigger_payload: dict[str, Any], description: str
|
||||
) -> dict[str, Any]:
|
||||
"""Build action variables."""
|
||||
payload = {
|
||||
"trigger": {
|
||||
**trigger_info["trigger_data"],
|
||||
CONF_PLATFORM: trigger_key,
|
||||
"description": description,
|
||||
**extra_trigger_payload,
|
||||
}
|
||||
}
|
||||
if CONF_VARIABLES in conf:
|
||||
trigger_variables = conf[CONF_VARIABLES]
|
||||
payload.update(trigger_variables.async_render(hass, payload))
|
||||
return payload
|
||||
|
||||
# Wrap sync action so that it is always async.
|
||||
# This should be removed when sync actions are no longer supported.
|
||||
match get_hassjob_callable_job_type(action):
|
||||
case HassJobType.Executor:
|
||||
original_action = action
|
||||
|
||||
async def wrapped_executor_action(
|
||||
run_variables: dict[str, Any], context: Context | None = None
|
||||
) -> Any:
|
||||
"""Wrap sync action to be called in executor."""
|
||||
return await hass.async_add_executor_job(
|
||||
original_action, run_variables, context
|
||||
)
|
||||
|
||||
action = wrapped_executor_action
|
||||
|
||||
case HassJobType.Callback:
|
||||
original_action = action
|
||||
|
||||
async def wrapped_callback_action(
|
||||
run_variables: dict[str, Any], context: Context | None = None
|
||||
) -> Any:
|
||||
"""Wrap callback action to be awaitable."""
|
||||
return original_action(run_variables, context)
|
||||
|
||||
action = wrapped_callback_action
|
||||
|
||||
trigger = trigger_cls(
|
||||
hass,
|
||||
TriggerConfig(
|
||||
key=trigger_key,
|
||||
target=conf.get(CONF_TARGET),
|
||||
options=conf.get(CONF_OPTIONS),
|
||||
),
|
||||
)
|
||||
return await trigger.async_attach_action(action, action_payload_builder)
|
||||
|
||||
|
||||
async def async_initialize_triggers(
|
||||
hass: HomeAssistant,
|
||||
trigger_config: list[ConfigType],
|
||||
@@ -657,17 +532,23 @@ async def async_initialize_triggers(
|
||||
trigger_data=trigger_data,
|
||||
)
|
||||
|
||||
action_wrapper = _trigger_action_wrapper(hass, action, conf)
|
||||
if hasattr(platform, "async_get_triggers"):
|
||||
trigger_descriptors = await platform.async_get_triggers(hass)
|
||||
relative_trigger_key = get_relative_description_key(
|
||||
platform_domain, trigger_key
|
||||
)
|
||||
trigger_cls = trigger_descriptors[relative_trigger_key]
|
||||
coro = _async_attach_trigger_cls(
|
||||
hass, trigger_cls, trigger_key, conf, action, info
|
||||
trigger = trigger_cls(
|
||||
hass,
|
||||
TriggerConfig(
|
||||
key=trigger_key,
|
||||
target=conf.get(CONF_TARGET),
|
||||
options=conf.get(CONF_OPTIONS),
|
||||
),
|
||||
)
|
||||
coro = trigger.async_attach(action_wrapper, info)
|
||||
else:
|
||||
action_wrapper = _trigger_action_wrapper(hass, action, conf)
|
||||
coro = platform.async_attach_trigger(hass, conf, action_wrapper, info)
|
||||
|
||||
triggers.append(create_eager_task(coro))
|
||||
|
@@ -15,7 +15,7 @@ astral==2.2
|
||||
async-interrupt==1.2.2
|
||||
async-upnp-client==0.45.0
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==25.4.0
|
||||
attrs==25.3.0
|
||||
audioop-lts==0.2.1
|
||||
av==13.1.0
|
||||
awesomeversion==25.5.0
|
||||
|
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"annotatedyaml==1.0.2",
|
||||
"astral==2.2",
|
||||
"async-interrupt==1.2.2",
|
||||
"attrs==25.4.0",
|
||||
"attrs==25.3.0",
|
||||
"atomicwrites-homeassistant==1.4.1",
|
||||
"audioop-lts==0.2.1",
|
||||
"awesomeversion==25.5.0",
|
||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@@ -13,7 +13,7 @@ aiozoneinfo==0.2.3
|
||||
annotatedyaml==1.0.2
|
||||
astral==2.2
|
||||
async-interrupt==1.2.2
|
||||
attrs==25.4.0
|
||||
attrs==25.3.0
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
audioop-lts==0.2.1
|
||||
awesomeversion==25.5.0
|
||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -2209,9 +2209,6 @@ pynetio==0.1.9.1
|
||||
# homeassistant.components.nina
|
||||
pynina==0.3.6
|
||||
|
||||
# homeassistant.components.nintendo_parental
|
||||
pynintendoparental==1.0.1
|
||||
|
||||
# homeassistant.components.nobo_hub
|
||||
pynobo==1.8.1
|
||||
|
||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -1845,9 +1845,6 @@ pynetgear==0.10.10
|
||||
# homeassistant.components.nina
|
||||
pynina==0.3.6
|
||||
|
||||
# homeassistant.components.nintendo_parental
|
||||
pynintendoparental==1.0.1
|
||||
|
||||
# homeassistant.components.nobo_hub
|
||||
pynobo==1.8.1
|
||||
|
||||
|
@@ -517,6 +517,20 @@ MOCK_SUBENTRY_NUMBER_COMPONENT_NO_UNIT = {
|
||||
"entity_picture": "https://example.com/f9261f6feed443e7b7d5f3fbe2a47414",
|
||||
},
|
||||
}
|
||||
MOCK_SUBENTRY_SELECT_COMPONENT = {
|
||||
"fa261f6feed443e7b7d5f3fbe2a47414": {
|
||||
"platform": "select",
|
||||
"name": "Mode",
|
||||
"entity_category": None,
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value }}",
|
||||
"state_topic": "test-topic",
|
||||
"options": ["beer", "milk"],
|
||||
"value_template": "{{ value_json.value }}",
|
||||
"retain": False,
|
||||
"entity_picture": "https://example.com/fa261f6feed443e7b7d5f3fbe2a47414",
|
||||
},
|
||||
}
|
||||
MOCK_SUBENTRY_SENSOR_COMPONENT = {
|
||||
"e9261f6feed443e7b7d5f3fbe2a47412": {
|
||||
"platform": "sensor",
|
||||
@@ -668,6 +682,10 @@ MOCK_NUMBER_SUBENTRY_DATA_NO_UNIT = {
|
||||
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
|
||||
"components": MOCK_SUBENTRY_NUMBER_COMPONENT_NO_UNIT,
|
||||
}
|
||||
MOCK_SELECT_SUBENTRY_DATA = {
|
||||
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
|
||||
"components": MOCK_SUBENTRY_SELECT_COMPONENT,
|
||||
}
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE = {
|
||||
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
|
||||
"components": MOCK_SUBENTRY_SENSOR_COMPONENT,
|
||||
|
@@ -53,6 +53,7 @@ from .common import (
|
||||
MOCK_NUMBER_SUBENTRY_DATA_CUSTOM_UNIT,
|
||||
MOCK_NUMBER_SUBENTRY_DATA_DEVICE_CLASS_UNIT,
|
||||
MOCK_NUMBER_SUBENTRY_DATA_NO_UNIT,
|
||||
MOCK_SELECT_SUBENTRY_DATA,
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE,
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS,
|
||||
@@ -3553,6 +3554,24 @@ async def test_migrate_of_incompatible_config_entry(
|
||||
"Milk notifier Speed",
|
||||
id="number_no_unit",
|
||||
),
|
||||
pytest.param(
|
||||
MOCK_SELECT_SUBENTRY_DATA,
|
||||
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
|
||||
{"name": "Mode"},
|
||||
{},
|
||||
(),
|
||||
{
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value }}",
|
||||
"state_topic": "test-topic",
|
||||
"options": ["beer", "milk"],
|
||||
"value_template": "{{ value_json.value }}",
|
||||
"retain": False,
|
||||
},
|
||||
(),
|
||||
"Milk notifier Mode",
|
||||
id="select",
|
||||
),
|
||||
pytest.param(
|
||||
MOCK_SENSOR_SUBENTRY_DATA_SINGLE,
|
||||
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
|
||||
|
@@ -1 +0,0 @@
|
||||
"""Tests for the Nintendo Switch Parental Controls integration."""
|
@@ -1,93 +0,0 @@
|
||||
"""Common fixtures for the Nintendo Switch Parental Controls tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pynintendoparental.device import Device
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.nintendo_parental.const import DOMAIN
|
||||
|
||||
from .const import ACCOUNT_ID, API_TOKEN, LOGIN_URL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"session_token": API_TOKEN},
|
||||
unique_id=ACCOUNT_ID,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_nintendo_device() -> Device:
|
||||
"""Return a mocked device."""
|
||||
mock = AsyncMock(spec=Device)
|
||||
mock.device_id = "testdevid"
|
||||
mock.name = "Home Assistant Test"
|
||||
mock.extra = {"device": {"firmwareVersion": {"displayedVersion": "99.99.99"}}}
|
||||
mock.limit_time = 120
|
||||
mock.today_playing_time = 110
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_nintendo_authenticator() -> Generator[MagicMock]:
|
||||
"""Mock Nintendo Authenticator."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.nintendo_parental.Authenticator",
|
||||
autospec=True,
|
||||
) as mock_auth_class,
|
||||
patch(
|
||||
"homeassistant.components.nintendo_parental.config_flow.Authenticator",
|
||||
new=mock_auth_class,
|
||||
),
|
||||
):
|
||||
mock_auth = MagicMock()
|
||||
mock_auth._id_token = API_TOKEN
|
||||
mock_auth._at_expiry = datetime(2099, 12, 31, 23, 59, 59)
|
||||
mock_auth.account_id = ACCOUNT_ID
|
||||
mock_auth.login_url = LOGIN_URL
|
||||
mock_auth.get_session_token = API_TOKEN
|
||||
# Patch complete_login as an AsyncMock on both instance and class as this is a class method
|
||||
mock_auth.complete_login = AsyncMock()
|
||||
type(mock_auth).complete_login = mock_auth.complete_login
|
||||
mock_auth_class.generate_login.return_value = mock_auth
|
||||
yield mock_auth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_nintendo_client(
|
||||
mock_nintendo_device: Device,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock a Nintendo client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.nintendo_parental.NintendoParental",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.nintendo_parental.config_flow.NintendoParental",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.update.return_value = True
|
||||
client.devices.return_value = {"testdevid": mock_nintendo_device}
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.nintendo_parental.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
@@ -1,5 +0,0 @@
|
||||
"""Constants for the Nintendo Parental Controls test suite."""
|
||||
|
||||
ACCOUNT_ID = "aabbccddee112233"
|
||||
API_TOKEN = "valid_token"
|
||||
LOGIN_URL = "http://example.com"
|
@@ -1,101 +0,0 @@
|
||||
"""Test the Nintendo Switch Parental Controls config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pynintendoparental.exceptions import InvalidSessionTokenException
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.nintendo_parental.const import CONF_SESSION_TOKEN, DOMAIN
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .const import ACCOUNT_ID, API_TOKEN, LOGIN_URL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_nintendo_authenticator: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a full and successful config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result is not None
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert "link" in result["description_placeholders"]
|
||||
assert result["description_placeholders"]["link"] == LOGIN_URL
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: API_TOKEN}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ACCOUNT_ID
|
||||
assert result["data"][CONF_SESSION_TOKEN] == API_TOKEN
|
||||
assert result["result"].unique_id == ACCOUNT_ID
|
||||
|
||||
|
||||
async def test_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_nintendo_authenticator: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the flow aborts if the account is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: API_TOKEN}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
mock_nintendo_authenticator: AsyncMock,
|
||||
) -> None:
|
||||
"""Test handling of invalid authentication."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result is not None
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert "link" in result["description_placeholders"]
|
||||
|
||||
# Simulate invalid authentication by raising an exception
|
||||
mock_nintendo_authenticator.complete_login.side_effect = (
|
||||
InvalidSessionTokenException
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: "invalid_token"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
# Now ensure that the flow can be recovered
|
||||
mock_nintendo_authenticator.complete_login.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: API_TOKEN}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ACCOUNT_ID
|
||||
assert result["data"][CONF_SESSION_TOKEN] == API_TOKEN
|
||||
assert result["result"].unique_id == ACCOUNT_ID
|
@@ -24,7 +24,9 @@ from homeassistant.helpers.trigger import (
|
||||
DATA_PLUGGABLE_ACTIONS,
|
||||
PluggableAction,
|
||||
Trigger,
|
||||
TriggerActionRunner,
|
||||
TriggerActionType,
|
||||
TriggerConfig,
|
||||
TriggerInfo,
|
||||
_async_get_trigger_platform,
|
||||
async_initialize_triggers,
|
||||
async_validate_trigger_config,
|
||||
@@ -447,31 +449,7 @@ async def test_pluggable_action(
|
||||
assert not plug_2
|
||||
|
||||
|
||||
class TriggerActionFunctionTypeHelper:
|
||||
"""Helper for testing different trigger action function types."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Init helper."""
|
||||
self.action_calls = []
|
||||
|
||||
@callback
|
||||
def cb_action(self, *args):
|
||||
"""Callback action."""
|
||||
self.action_calls.append([*args])
|
||||
|
||||
def sync_action(self, *args):
|
||||
"""Sync action."""
|
||||
self.action_calls.append([*args])
|
||||
|
||||
async def async_action(self, *args):
|
||||
"""Async action."""
|
||||
self.action_calls.append([*args])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action_method", ["cb_action", "sync_action", "async_action"])
|
||||
async def test_platform_multiple_triggers(
|
||||
hass: HomeAssistant, action_method: str
|
||||
) -> None:
|
||||
async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
|
||||
"""Test a trigger platform with multiple trigger."""
|
||||
|
||||
class MockTrigger(Trigger):
|
||||
@@ -484,23 +462,30 @@ async def test_platform_multiple_triggers(
|
||||
"""Validate config."""
|
||||
return config
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
|
||||
class MockTrigger1(MockTrigger):
|
||||
"""Mock trigger 1."""
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
run_action({"extra": "test_trigger_1"}, "trigger 1 desc")
|
||||
action({"trigger": "test_trigger_1"})
|
||||
|
||||
class MockTrigger2(MockTrigger):
|
||||
"""Mock trigger 2."""
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
run_action({"extra": "test_trigger_2"}, "trigger 2 desc")
|
||||
action({"trigger": "test_trigger_2"})
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
return {
|
||||
@@ -523,41 +508,22 @@ async def test_platform_multiple_triggers(
|
||||
|
||||
log_cb = MagicMock()
|
||||
|
||||
action_helper = TriggerActionFunctionTypeHelper()
|
||||
action_method = getattr(action_helper, action_method)
|
||||
action_calls = []
|
||||
|
||||
await async_initialize_triggers(hass, config_1, action_method, "test", "", log_cb)
|
||||
assert len(action_helper.action_calls) == 1
|
||||
assert action_helper.action_calls[0][0] == {
|
||||
"trigger": {
|
||||
"alias": None,
|
||||
"description": "trigger 1 desc",
|
||||
"extra": "test_trigger_1",
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "test",
|
||||
}
|
||||
}
|
||||
action_helper.action_calls.clear()
|
||||
@callback
|
||||
def cb_action(*args):
|
||||
action_calls.append([*args])
|
||||
|
||||
await async_initialize_triggers(hass, config_2, action_method, "test", "", log_cb)
|
||||
assert len(action_helper.action_calls) == 1
|
||||
assert action_helper.action_calls[0][0] == {
|
||||
"trigger": {
|
||||
"alias": None,
|
||||
"description": "trigger 2 desc",
|
||||
"extra": "test_trigger_2",
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "test.trig_2",
|
||||
}
|
||||
}
|
||||
action_helper.action_calls.clear()
|
||||
await async_initialize_triggers(hass, config_1, cb_action, "test", "", log_cb)
|
||||
assert action_calls == [[{"trigger": "test_trigger_1"}]]
|
||||
action_calls.clear()
|
||||
|
||||
await async_initialize_triggers(hass, config_2, cb_action, "test", "", log_cb)
|
||||
assert action_calls == [[{"trigger": "test_trigger_2"}]]
|
||||
action_calls.clear()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
await async_initialize_triggers(
|
||||
hass, config_3, action_method, "test", "", log_cb
|
||||
)
|
||||
await async_initialize_triggers(hass, config_3, cb_action, "test", "", log_cb)
|
||||
|
||||
|
||||
async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:
|
||||
|
Reference in New Issue
Block a user