Change attributes in AdaptiveSwitch via service call (#443)

This commit is contained in:
Benjamin Auquite 2023-03-26 22:47:50 -07:00 committed by Bas Nijholt
commit c05e0cc244
5 changed files with 366 additions and 23 deletions

View file

@ -41,6 +41,9 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]):
# This will reload any changes the user made to any YAML configurations.
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
# quickly populate data for change_switch_settings before actually loading integration.
# update_services_yaml()
if DOMAIN in config:
for entry in config[DOMAIN]:
hass.async_create_task(

View file

@ -66,6 +66,8 @@ SERVICE_SET_MANUAL_CONTROL = "set_manual_control"
CONF_MANUAL_CONTROL = "manual_control"
SERVICE_APPLY = "apply"
CONF_TURN_ON_LIGHTS = "turn_on_lights"
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults"
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
TURNING_OFF_DELAY = 5

View file

@ -41,6 +41,7 @@ apply:
example: false
selector:
boolean:
set_manual_control:
description: Mark whether a light is 'manually controlled'.
fields:
@ -65,3 +66,180 @@ set_manual_control:
default: true
selector:
boolean:
change_switch_settings:
description: "Change any settings you'd like in the switch. All options here are the same as in the config flow."
fields:
entity_id:
description: "entity_id of the Adaptive Lighting switch."
required: true
selector:
entity:
domain: switch
use_defaults:
description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation."
example: "current"
required: false
default: "current"
selector:
select:
options:
- "current"
- "configuration"
- "factory"
turn_on_lights:
description: "Turn on the lights that are off, default: false"
example: false
required: false
selector:
boolean:
initial_transition:
description: "initial_transition: When lights turn 'off' to 'on'. (seconds)"
example: 1
required: false
selector:
text:
sleep_transition:
description: "sleep_transition: When 'sleep_state' changes. (seconds)"
example: 1
required: false
selector:
text:
max_brightness:
description: "max_brightness: Highest brightness of lights during a cycle. (%)"
required: false
example: 100
selector:
text:
max_color_temp:
description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)"
required: false
example: 5500
selector:
text:
min_brightness:
description: "min_brightness: Lowest brightness of lights during a cycle. (%)"
required: false
example: 1
selector:
text:
min_color_temp:
description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)"
required: false
example: 2000
selector:
text:
only_once:
description: "only_once: Only adapt the lights when turning them on."
example: false
required: false
selector:
boolean:
prefer_rgb_color:
description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible."
required: false
example: false
selector:
boolean:
separate_turn_on_commands:
description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)."
required: false
example: false
selector:
boolean:
send_split_delay:
description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly."
required: false
example: 0
selector:
boolean:
sleep_brightness:
description: "sleep_brightness, Brightness setting for Sleep Mode. (%)"
required: false
example: 1
selector:
text:
sleep_rgb_or_color_temp:
description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'"
required: false
example: "color_temp"
selector:
select:
options:
- "rgb_color"
- "color_temp"
sleep_rgb_color:
description: "sleep_rgb_color, in RGB"
required: false
selector:
color_rgb:
sleep_color_temp:
description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)"
required: false
example: 1000
selector:
text:
sunrise_offset:
description: sunrise_offset, in +/- seconds (integer)
required: false
example: 0
selector:
number:
min: 0
max: 86300
sunrise_time:
description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)
required: false
example: ""
selector:
time:
sunset_offset:
description: sunset_offset, in +/- seconds (integer)
required: false
example: ""
selector:
number:
min: 0
max: 86300
sunset_time:
description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)
example: ""
required: false
selector:
time:
max_sunrise_time:
description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)"
example: ""
required: false
selector:
time:
min_sunset_time:
description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)"
example: ""
required: false
selector:
time:
take_over_control:
description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on."
required: false
example: true
selector:
boolean:
detect_non_ha_changes:
description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)"
required: false
example: false
selector:
boolean:
transition:
description: "Transition time when applying a change to the lights (seconds)"
required: false
example: 45
selector:
text:
adapt_delay:
description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
required: false
example: 0
selector:
text:

View file

@ -72,7 +72,7 @@ from homeassistant.core import (
State,
callback,
)
from homeassistant.helpers import entity_registry
from homeassistant.helpers import entity_platform, entity_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
async_track_state_change_event,
@ -125,6 +125,7 @@ from .const import (
CONF_TAKE_OVER_CONTROL,
CONF_TRANSITION,
CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS,
DOMAIN,
EXTRA_VALIDATION,
ICON_BRIGHTNESS,
@ -132,6 +133,7 @@ from .const import (
ICON_MAIN,
ICON_SLEEP,
SERVICE_APPLY,
SERVICE_CHANGE_SWITCH_SETTINGS,
SERVICE_SET_MANUAL_CONTROL,
SLEEP_MODE_SWITCH,
SUN_EVENT_MIDNIGHT,
@ -187,6 +189,11 @@ def _int_to_bytes(i: int, signed: bool = False) -> bytes:
return i.to_bytes((bits + 7) // 8, "little", signed=signed)
def int_between(min_int, max_int):
"""Return an integer between 'min_int' and 'max_int'."""
return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int))
def _short_hash(string: str, length: int = 4) -> str:
"""Create a hash of 'string' with length 'length'."""
str_hash_bytes = _int_to_bytes(hash(string), signed=True)
@ -349,6 +356,48 @@ def _get_switches_from_service_call(
raise ValueError("adaptive-lighting: User sent incorrect data to service call")
async def handle_change_switch_settings(
switch: AdaptiveSwitch, service_call: ServiceCall
):
"""Allows HASS to change config values via a service call."""
data = service_call.data
which = data.get(CONF_USE_DEFAULTS, "current")
if which == "current": # use whatever we're already using.
defaults = switch._current_settings # pylint: disable=protected-access
# not needed since validate() does this part for us
elif which == "factory": # use actual defaults listed in the documentation
defaults = {key: default for key, default, _ in VALIDATION_TUPLES}
elif (
which == "configuration"
): # use whatever's in the config flow or configuration.yaml
defaults = switch._config_backup # pylint: disable=protected-access
else:
defaults = None
switch._set_changeable_settings(
data=data,
defaults=defaults,
)
_LOGGER.debug(
"Called 'adaptive_lighting.change_switch_settings' service with '%s'",
data,
)
all_lights = switch._lights # pylint: disable=protected-access
switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False)
# pylint: disable=protected-access
if switch.is_on:
await switch._update_attrs_and_maybe_adapt_lights(
all_lights,
transition=switch._initial_transition,
force=True,
context=switch.create_context("service", parent=service_call.context),
)
# pylint: enable=protected-access
@callback
def _fire_manual_control_event(
switch: AdaptiveSwitch, light: str, context: Context, is_async=True
@ -506,13 +555,38 @@ async def async_setup_entry(
),
)
args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string}
for k, _, valid in VALIDATION_TUPLES:
# Modifying these after initialization isn't possible (yet)
if k != CONF_INTERVAL and k != CONF_NAME and k != CONF_LIGHTS:
args[vol.Optional(k)] = valid
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_CHANGE_SWITCH_SETTINGS,
args,
handle_change_switch_settings,
)
def validate(config_entry: ConfigEntry):
def validate(config_entry: ConfigEntry, **kwargs):
"""Get the options and data from the config_entry and add defaults."""
defaults = {key: default for key, default, _ in VALIDATION_TUPLES}
data = deepcopy(defaults)
data.update(config_entry.options) # come from options flow
data.update(config_entry.data) # all yaml settings come from data
# defaults and data will exist only if this is called from change_switch_settings
defaults = kwargs.get("defaults")
service_data = kwargs.get("data")
if defaults is None:
defaults = {key: default for key, default, _ in VALIDATION_TUPLES}
if config_entry is not None:
# Is this deepcopy necessary?
# We're already creating a new array for the defaults variable...
# afterwards defaults is never used again.
data = deepcopy(defaults)
data.update(config_entry.options) # come from options flow
data.update(config_entry.data) # all yaml settings come from data
else:
# no idea how to clear original data from memory
# hopefully it does it automatically, otherwise TODO.
data = deepcopy(defaults)
data.update(service_data)
data = {key: replace_none_str(value) for key, value in data.items()}
for key, (validate_value, _) in EXTRA_VALIDATION.items():
value = data.get(key)
@ -682,31 +756,25 @@ def _attributes_have_changed(
class AdaptiveSwitch(SwitchEntity, RestoreEntity):
"""Representation of a Adaptive Lighting switch."""
def __init__(
def _set_changeable_settings(
self,
hass,
config_entry: ConfigEntry,
turn_on_off_listener: TurnOnOffListener,
sleep_mode_switch: SimpleSwitch,
adapt_color_switch: SimpleSwitch,
adapt_brightness_switch: SimpleSwitch,
data: dict,
defaults: dict,
):
"""Initialize the Adaptive Lighting switch."""
self.hass = hass
self.turn_on_off_listener = turn_on_off_listener
self.sleep_mode_switch = sleep_mode_switch
self.adapt_color_switch = adapt_color_switch
self.adapt_brightness_switch = adapt_brightness_switch
# Should only contain the settings we want the users to be able to change during runtime.
data = validate(
config_entry=None,
data=data,
defaults=defaults,
)
data = validate(config_entry)
self._name = data[CONF_NAME]
self._lights = data[CONF_LIGHTS]
# backup data for use in change_switch_settings "current" CONF_USE_DEFAULTS
self._current_settings = data
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES]
self._initial_transition = data[CONF_INITIAL_TRANSITION]
self._sleep_transition = data[CONF_SLEEP_TRANSITION]
self._interval = data[CONF_INTERVAL]
self._only_once = data[CONF_ONLY_ONCE]
self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR]
self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS]
@ -742,6 +810,43 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
time_zone=self.hass.config.time_zone,
transition=data[CONF_TRANSITION],
)
_LOGGER.debug(
"%s: Set switch settings for lights '%s'. now using data: '%s'",
self._name,
self._lights,
data,
)
def __init__(
self,
hass,
config_entry: ConfigEntry,
turn_on_off_listener: TurnOnOffListener,
sleep_mode_switch: SimpleSwitch,
adapt_color_switch: SimpleSwitch,
adapt_brightness_switch: SimpleSwitch,
):
"""Initialize the Adaptive Lighting switch."""
# Set attributes we DON'T want users modifying
# during runtime here.
self.hass = hass
self.turn_on_off_listener = turn_on_off_listener
self.sleep_mode_switch = sleep_mode_switch
self.adapt_color_switch = adapt_color_switch
self.adapt_brightness_switch = adapt_brightness_switch
data = validate(config_entry)
self._name = data[CONF_NAME]
self._interval = data[CONF_INTERVAL]
self._lights = data[CONF_LIGHTS]
# backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS
self._config_backup = deepcopy(data)
self._set_changeable_settings(
data=data,
defaults=None,
)
# Set other attributes
self._icon = ICON_MAIN