Skip to content

Instantly share code, notes, and snippets.

@cemizm
Last active January 1, 2025 14:34
Show Gist options
  • Select an option

  • Save cemizm/15b5a2cf6ea23525f5ea4e90a52be373 to your computer and use it in GitHub Desktop.

Select an option

Save cemizm/15b5a2cf6ea23525f5ea4e90a52be373 to your computer and use it in GitHub Desktop.
AppDaemon App: Advanced Motion Activated Light
import appdaemon.plugins.hass.hassapi as hass
from enum import Enum
class State(Enum):
Idle = 0
Nearby = 1
Detected = 2
Manual = 3
Cooldown = 4
def __str__(self):
return f'{self.name}'
@classmethod
def from_str(cls, str_value, default=None):
return cls[str_value] if str_value in cls._member_names_ else default or cls.Idle
class Action(Enum):
Init = 0
Motion = 1
Illuminance = 2
NearbyMotion = 3
Timeout = 4
Override = 5
class MotionLight(hass.Hass):
def initialize(self):
# Configuration
self.main_motion_entities = self.args["main_motion_entities"]
self.nearby_motion_entities = self.args.get("nearby_motion_entities", [])
self.brightness_entities = self.args.get("brightness_entities", [])
self.output_entity = self.args["output_entity"]
self.brightness_threshold = self.args.get("brightness_threshold", 1500)
self.timeout_nearby = self.args.get("timeout_nearby", 10)
self.timeout_detected = self.args.get("timeout_detected", 2 * 60)
self.timeout_manual_off = self.args.get("timeout_manual_off", 2 * 60)
self.timeout_manual_on = self.args.get("timeout_manual_on", 5 * 60)
self.timeout_nearby_throttle = self.args.get("timeout_nearby_throttle", 20)
# last states
self.state_name = f"sensor.{self.name}"
self.state_expected = None
self.handle = None
# main motion handlers
for entity in self.main_motion_entities:
self.listen_state(self.update_attributes, entity, action=Action.Motion)
# additional motion handlers
for entity in self.nearby_motion_entities:
self.listen_state(self.update_attributes, entity, action=Action.NearbyMotion)
# illuminance handlers
for entity in self.brightness_entities:
self.listen_state(self.update_attributes, entity, action=Action.Illuminance)
# manual off handling
self.listen_state(self.update_manual_override, self.output_entity)
# after restart
if self.get_state(self.state_name) is None:
self.current_state = State.Manual
self.update_attributes(None, None, None, None, {'action': Action.Init})
def update_attributes(self, entity, attribute, old, new, kwargs):
#self.log(f"{entity}.{attribute} {old} -> {new}")
self.update_main_motion()
self.update_nearby_motion()
self.update_illuminance()
self.update(kwargs['action'])
def update_main_motion(self):
motion = False
for entity in self.main_motion_entities:
if self.get_state(entity) == "on":
motion = True
self.motion = motion
def update_nearby_motion(self):
motion = False
for entity in self.nearby_motion_entities:
if self.get_state(entity) in ["on", "Detected"]:
motion = True
self.nearby_motion = motion
def update_illuminance(self):
collected = []
for entity in self.brightness_entities:
state = self.get_state(entity)
if state is not None:
collected.append(float(state))
self.illuminance = min(collected, default=0)
def update_manual_override(self, entity, attribute, old, new, kwargs):
if self.state_expected is None or new != self.state_expected:
self.update(Action.Override)
self.state_expected = None
self.output_state = new
def update_timeout(self, kwargs):
self.handle = None
self.update(Action.Timeout)
def update(self, action: Action):
#self.log(f"{action} ----> {self.current_state} {self.motion} {self.nearby_motion} {self.illuminance}")
new_state, retrigger = self.process_action(action)
if (self.current_state != new_state) or retrigger:
#self.log(f"{self.current_state} -> {new_state} retrigger: {retrigger}")
self.current_state = new_state
if new_state == State.Nearby:
self.set_output("on")
self.set_timer(self.timeout_nearby)
elif new_state == State.Detected:
self.set_output("on")
self.set_timer(self.timeout_detected if not self.motion else 0)
elif new_state == State.Manual:
self.set_timer(self.timeout_manual_on if self.is_output_on() else self.timeout_manual_off)
elif new_state == State.Cooldown:
self.set_output("off")
self.set_timer(self.timeout_nearby_throttle)
elif new_state == State.Idle:
self.set_output("off")
self.set_timer(0)
def process_action(self, action):
retrigger = False
new_state = self.current_state
if action == Action.Init:
retrigger = True
elif action == Action.Override:
new_state = State.Manual
retrigger = True
else:
if self.current_state == State.Idle:
if self.illuminance < self.brightness_threshold:
if self.motion:
new_state = State.Detected
elif action == Action.NearbyMotion and self.nearby_motion:
new_state = State.Nearby
elif self.current_state == State.Nearby:
if action == Action.Timeout:
new_state = State.Cooldown
elif action == Action.Motion and self.motion:
new_state = State.Detected
elif self.current_state == State.Detected:
if action == Action.Timeout:
new_state = State.Idle
elif action == Action.Motion:
retrigger = True
elif action == Action.Init:
retrigger = True
elif self.current_state == State.Manual:
if action == Action.Timeout:
new_state = State.Idle
elif self.current_state == State.Cooldown:
if action == Action.Timeout:
new_state = State.Idle
elif self.motion:
new_state = State.Detected
return new_state,retrigger
# timer and output entity
def set_output(self, state):
self.state_expected = state
if self.get_state(self.output_entity) != state:
#self.log(f"{self.output_entity}: {state}")
if state == "on":
self.turn_on(self.output_entity)
else:
self.turn_off(self.output_entity)
def is_output_on(self):
return self.get_state(self.output_entity, default="off") == "on"
def set_timer(self, timeout):
if self.handle is not None:
self.cancel_timer(self.handle)
self.handle = None
if timeout != 0:
self.handle = self.run_in(self.update_timeout, timeout)
# properties
@property
def current_state(self):
return State.from_str(self.get_state(self.state_name))
@current_state.setter
def current_state(self, value):
self.set_state(self.state_name, state=value.name)
@property
def motion(self):
return self.get_state(self.state_name, attribute="motion", default=False)
@motion.setter
def motion(self, value):
self.set_state(self.state_name, attributes={"motion": value})
@property
def nearby_motion(self):
return self.get_state(self.state_name, attribute="nearby_motion", default=False)
@nearby_motion.setter
def nearby_motion(self, value):
self.set_state(self.state_name, attributes={"nearby_motion": value})
@property
def illuminance(self):
return self.get_state(self.state_name, attribute="illuminance", default=0)
@illuminance.setter
def illuminance(self, value):
self.set_state(self.state_name, attributes={"illuminance": value})
@property
def output_state(self):
return self.get_state(self.state_name, attribute="output_state", default=0)
@output_state.setter
def output_state(self, value):
self.set_state(self.state_name, attributes={"output_state": value})
hall_motion_light:
class: MotionLight
module: motion_light
main_motion_entities:
- binary_sensor.hall_motion_bath
- binary_sensor.hall_motion_entry
nearby_motion_entities:
- sensor.entry_motion_light
brightness_entities:
- sensor.hall_motion_entry_illuminance
- sensor.hall_motion_bath_illuminance
output_entity: light.hall_ceiling
timeout_detected: 120
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment