This commit is contained in:
Bas Nijholt 2020-10-14 07:50:45 +02:00
commit d67479d017
6 changed files with 55 additions and 26 deletions

View file

@ -40,7 +40,8 @@ ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener"
UNDO_UPDATE_LISTENER = "undo_update_listener"
NONE_STR = "None"
SERVICE_NOT_MANUALLY_CONTROLLED = "not_manually_controlled"
SERVICE_SET_MANUALLY_CONTROLLED = "set_manually_controlled"
CONF_MANUALLY_CONTROLLED = "manually_controlled"
SERVICE_APPLY = "apply"
CONF_TURN_ON_LIGHTS = "turn_on_lights"

View file

@ -4,6 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/adaptive_lighting",
"config_flow": true,
"dependencies": [],
"codeowners": ["@basnijholt", "@claytonjn"],
"codeowners": ["@basnijholt"],
"requirements": []
}

View file

@ -22,12 +22,15 @@ apply:
turn_on_lights:
description: "Turn on the lights that are off, default: false"
example: false
not_manually_controlled:
description: Mark a light as not being 'manually_controlled'.
set_manually_controlled:
description: Mark a light as (not) being 'manually_controlled'.
fields:
entity_id:
description: entity_id of the Adaptive Lighting switch.
example: switch.adaptive_lighting_default
manually_controlled:
description: "Whether to add ('true') or remove ('false') the light from the 'manually_controlled' list, default: true"
example: true
lights:
description: entity_id(s) of lights.
example: light.bedroom_ceiling

View file

@ -35,9 +35,9 @@
"sleep_brightness": "sleep_brightness, in %",
"sleep_color_temp": "sleep_color_temp, in Kelvin",
"sunrise_offset": "sunrise_offset, in +/- seconds",
"sunrise_time": "sunrise_time, in 'HH:MM:SS' format",
"sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)",
"sunset_offset": "sunset_offset, in +/- seconds",
"sunset_time": "sunset_time, in 'HH:MM:SS' format",
"sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)",
"take_over_control": "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.",
"detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
"transition": "transition, in seconds"

View file

@ -85,6 +85,7 @@ from .const import (
CONF_INITIAL_TRANSITION,
CONF_INTERVAL,
CONF_LIGHTS,
CONF_MANUALLY_CONTROLLED,
CONF_MAX_BRIGHTNESS,
CONF_MAX_COLOR_TEMP,
CONF_MIN_BRIGHTNESS,
@ -104,7 +105,7 @@ from .const import (
EXTRA_VALIDATION,
ICON,
SERVICE_APPLY,
SERVICE_NOT_MANUALLY_CONTROLLED,
SERVICE_SET_MANUALLY_CONTROLLED,
SUN_EVENT_MIDNIGHT,
SUN_EVENT_NOON,
TURNING_OFF_DELAY,
@ -176,21 +177,39 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
)
async def handle_not_manually_controlled(
async def handle_set_manually_controlled(
switch: AdaptiveSwitch, service_call: ServiceCall
):
"""Remove lights from the 'manually_controlled' list."""
all_lights = _expand_light_groups(switch.hass, service_call.data[CONF_LIGHTS])
switch.turn_on_off_listener.reset(*all_lights)
_LOGGER.debug(
"Called 'adaptive_lighting.set_manually_controlled' service with '%s'",
service_call.data,
)
if service_call.data[CONF_MANUALLY_CONTROLLED]:
for light in all_lights:
switch.turn_on_off_listener.manually_controlled[light] = True
_fire_manual_control_event(switch.hass, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
# pylint: disable=protected-access
await switch._adapt_lights(
all_lights,
transition=switch._initial_transition,
force=True,
context=switch.create_context("service"),
)
@callback
def _fire_manually_controlled_event(
def _fire_manual_control_event(
hass: HomeAssistant, light: str, context: Context, is_async=True
):
"""Fire an event that 'light' is marked as manually_controlled."""
fire = hass.bus.async_fire if is_async else hass.bus.fire
fire(f"{DOMAIN}.manually_controlled", {ATTR_ENTITY_ID: light}, context=context)
# Calling the event_type='manually_controlled' would be better, but
# event_type has a 32 character limit.
fire(f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light}, context=context)
async def async_setup_entry(
@ -230,9 +249,12 @@ async def async_setup_entry(
)
platform.async_register_entity_service(
SERVICE_NOT_MANUALLY_CONTROLLED,
{vol.Required(CONF_LIGHTS): cv.entity_ids},
handle_not_manually_controlled,
SERVICE_SET_MANUALLY_CONTROLLED,
{
vol.Required(CONF_LIGHTS): cv.entity_ids,
vol.Optional(CONF_MANUALLY_CONTROLLED, default=True): cv.boolean,
},
handle_set_manually_controlled,
)
@ -557,6 +579,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# 'adapt_lgt_XXXX_adapt_lights_99999999'
# 'adapt_lgt_XXXX_sleep_999999999999999'
# 'adapt_lgt_XXXX_light_event_999999999'
# 'adapt_lgt_XXXX_service_9999999999999'
# So 100 million calls before we run into the 36 chars limit.
context = create_context(self._name, which, self._context_cnt)
self._context_cnt += 1
@ -639,6 +662,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
service_data[ATTR_COLOR_TEMP] = color_temp_mired
elif "color" in features and adapt_rgb_color:
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
context = context or self.create_context("adapt_lights")
if (
self._take_over_control
@ -675,6 +699,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
force: bool = False,
context: Optional[Context] = None,
) -> None:
assert context is not None
_LOGGER.debug(
"%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'",
self._name,
@ -698,6 +723,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
force: bool,
context: Optional[Context],
) -> None:
assert context is not None
_LOGGER.debug(
"%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called",
self.name,
@ -752,7 +778,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
entity_id,
event.context.id,
)
self.turn_on_off_listener.reset(entity_id)
self.turn_on_off_listener.reset(entity_id, reset_manually_controlled=False)
# Tracks 'off' → 'on' state changes
self._off_to_on_event[entity_id] = event
lock = self._locks.get(entity_id)
@ -1002,7 +1028,7 @@ class TurnOnOffListener:
# When a state is different `max_cnt_significant_changes` times in a row,
# mark it as manually_controlled.
self.max_cnt_significant_changes = 1
self.max_cnt_significant_changes = 2
self.remove_listener = self.hass.bus.async_listen(
EVENT_CALL_SERVICE, self.turn_on_off_event_listener
@ -1011,10 +1037,11 @@ class TurnOnOffListener:
EVENT_STATE_CHANGED, self.state_changed_event_listener
)
def reset(self, *lights) -> None:
def reset(self, *lights, reset_manually_controlled=True) -> None:
"""Reset the 'manually_controlled' status of the lights."""
for light in lights:
self.manually_controlled[light] = False
if reset_manually_controlled:
self.manually_controlled[light] = False
self.last_state_change.pop(light, None)
self.last_service_data.pop(light, None)
self.cnt_significant_changes[light] = 0
@ -1120,7 +1147,7 @@ class TurnOnOffListener:
# Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration.
manually_controlled = self.manually_controlled[light] = True
_fire_manually_controlled_event(self.hass, light, turn_on_event.context)
_fire_manual_control_event(self.hass, light, turn_on_event.context)
_LOGGER.debug(
"'%s' was already on and 'light.turn_on' was not called by the"
" adaptive_lighting integration (context.id='%s'), the Adaptive"
@ -1195,13 +1222,11 @@ class TurnOnOffListener:
if changed:
self.cnt_significant_changes[light] += 1
if n_changes >= self.max_cnt_significant_changes:
# Only mark a light as significantly changing, changed==True `x`
# times in a row. We do this because sometimes a state changes
# Only mark a light as significantly changing, if changed==True
# N times in a row. We do this because sometimes a state changes
# happens only *after* a new update interval has already started.
self.manually_controlled[light] = True
_fire_manually_controlled_event(
self.hass, light, context, is_async=False
)
_fire_manual_control_event(self.hass, light, context, is_async=False)
else:
if n_changes > 1:
_LOGGER.debug(

View file

@ -35,9 +35,9 @@
"sleep_brightness": "sleep_brightness, in %",
"sleep_color_temp": "sleep_color_temp, in Kelvin",
"sunrise_offset": "sunrise_offset, in +/- seconds",
"sunrise_time": "sunrise_time, in 'HH:MM:SS' format",
"sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)",
"sunset_offset": "sunset_offset, in +/- seconds",
"sunset_time": "sunset_time, in 'HH:MM:SS' format",
"sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)",
"take_over_control": "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.",
"detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
"transition": "transition, in seconds"