mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
Change attributes in AdaptiveSwitch via service call (#443)
This commit is contained in:
parent
0127496441
commit
c05e0cc244
5 changed files with 366 additions and 23 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue