Last active
January 1, 2025 14:34
-
-
Save cemizm/15b5a2cf6ea23525f5ea4e90a52be373 to your computer and use it in GitHub Desktop.
AppDaemon App: Advanced Motion Activated Light
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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