def _name(self, entity: str) -> Optional[str]: name: Optional[str] = None if self.cfg["show_friendly_name"]: name = self.friendly_name(entity) else: name = adu.hl_entity(entity) return name
def initialize(self) -> None: """Initialize a room with AutoMoLi.""" self.adu = ADutils(APP_NAME, config={}, icon=APP_ICON, ad=self) # python version check if not py37_or_higher: raise ValueError # set room self.room = str(self.args.get("room")) # sensor entities self.sensors = { "motion": self.args.pop("motion", set()), "humidity": self.args.get("humidity", set()), "illuminance": self.args.get("illuminance", set()), } # state values self.states = { "motion_on": self.args.get("motion_state_on", None), "motion_off": self.args.get("motion_state_off", None), } # threshold values self.thresholds = { "humidity": self.args.get("humidity_threshold"), "illuminance": self.args.get("illuminance_threshold"), } # on/off switch via input.boolean self.disable_switch_entity = self.args.get("disable_switch_entity", []) # print (*self.disable_switch_entity) # fade on/off delay self.fadeSetting = { "on": self.args.get("fade_on", DEFAULT_FADE_DURATION), "off": self.args.get("fade_off", DEFAULT_FADE_DURATION) } # currently active daytime settings self.active: Dict[str, Union[int, str]] = {} # lights_off callback handle self._handle = None # define light entities switched by automoli self.lights: Set[str] = self.args.get("lights", set()) if not self.lights: room_light_group = f"light.{self.room}" if self.entity_exists(room_light_group): self.lights.add(room_light_group) else: self.lights.update(self.find_sensors(KEYWORD_LIGHTS)) if not self.lights: raise ValueError(f"No lights available, sorry! ('{KEYWORD_LIGHTS}')") # define sensor entities monitored by automoli if not self.sensors["motion"]: self.sensors["motion"].update(self.find_sensors(KEYWORD_MOTION)) if not self.sensors["motion"]: raise ValueError(f"No sensors given/found, sorry! ('{KEYWORD_MOTION}')") # enumerate humidity sensors if threshold given if self.thresholds["humidity"] and not self.sensors["humidity"]: self.sensors["humidity"].update(self.find_sensors(KEYWORD_HUMIDITY)) if not self.sensors["humidity"]: self.log(f"No humidity sensors available → disabling blocker.") self.thresholds["humidity"] = None # enumerate illuminance sensors if threshold given if self.thresholds["illuminance"] and not self.sensors["illuminance"]: self.sensors["illuminance"].update(self.find_sensors(KEYWORD_ILLUMINANCE)) if not self.sensors["illuminance"]: self.log(f"No illuminance sensors available → disabling blocker.") self.thresholds["illuminance"] = None # use user-defined daytimes if available daytimes = self.build_daytimes(self.args.get("daytimes", DEFAULT_DAYTIMES)) # set up event listener for each sensor for sensor in self.sensors["motion"]: # listen to xiaomi sensors by default if not any([self.states["motion_on"], self.states["motion_off"]]): self.listen_event( self.motion_event, event=EVENT_MOTION_XIAOMI, entity_id=sensor ) self.refresh_timer() # do not use listen event and listen state below together continue # on/off-only sensors without events on every motion if all([self.states["motion_on"], self.states["motion_off"]]): self.listen_state( self.motion_detected, entity=sensor, new=self.states["motion_on"] ) self.listen_state( self.motion_cleared, entity=sensor, new=self.states["motion_off"] ) # display settings self.args.setdefault("listeners", self.sensors["motion"]) self.args.setdefault( "sensors_illuminance", list(self.sensors["illuminance"]) ) if self.sensors["illuminance"] else None self.args.setdefault( "sensors_humidity", list(self.sensors["humidity"]) ) if self.sensors["humidity"] else None self.args["daytimes"] = daytimes # init adutils # self.adu = ADutils( # APP_NAME, self.args, icon=APP_ICON, ad=self, show_config=True # ) self.adu.show_info(self.args)
class AutoMoLi(hass.Hass): # type: ignore """Automatic Motion Lights.""" def initialize(self) -> None: """Initialize a room with AutoMoLi.""" self.adu = ADutils(APP_NAME, config={}, icon=APP_ICON, ad=self) # python version check if not py37_or_higher: raise ValueError # set room self.room = str(self.args.get("room")) # sensor entities self.sensors = { "motion": self.args.pop("motion", set()), "humidity": self.args.get("humidity", set()), "illuminance": self.args.get("illuminance", set()), } # state values self.states = { "motion_on": self.args.get("motion_state_on", None), "motion_off": self.args.get("motion_state_off", None), } # threshold values self.thresholds = { "humidity": self.args.get("humidity_threshold"), "illuminance": self.args.get("illuminance_threshold"), } # on/off switch via input.boolean self.disable_switch_entity = self.args.get("disable_switch_entity", []) # print (*self.disable_switch_entity) # fade on/off delay self.fadeSetting = { "on": self.args.get("fade_on", DEFAULT_FADE_DURATION), "off": self.args.get("fade_off", DEFAULT_FADE_DURATION) } # currently active daytime settings self.active: Dict[str, Union[int, str]] = {} # lights_off callback handle self._handle = None # define light entities switched by automoli self.lights: Set[str] = self.args.get("lights", set()) if not self.lights: room_light_group = f"light.{self.room}" if self.entity_exists(room_light_group): self.lights.add(room_light_group) else: self.lights.update(self.find_sensors(KEYWORD_LIGHTS)) if not self.lights: raise ValueError(f"No lights available, sorry! ('{KEYWORD_LIGHTS}')") # define sensor entities monitored by automoli if not self.sensors["motion"]: self.sensors["motion"].update(self.find_sensors(KEYWORD_MOTION)) if not self.sensors["motion"]: raise ValueError(f"No sensors given/found, sorry! ('{KEYWORD_MOTION}')") # enumerate humidity sensors if threshold given if self.thresholds["humidity"] and not self.sensors["humidity"]: self.sensors["humidity"].update(self.find_sensors(KEYWORD_HUMIDITY)) if not self.sensors["humidity"]: self.log(f"No humidity sensors available → disabling blocker.") self.thresholds["humidity"] = None # enumerate illuminance sensors if threshold given if self.thresholds["illuminance"] and not self.sensors["illuminance"]: self.sensors["illuminance"].update(self.find_sensors(KEYWORD_ILLUMINANCE)) if not self.sensors["illuminance"]: self.log(f"No illuminance sensors available → disabling blocker.") self.thresholds["illuminance"] = None # use user-defined daytimes if available daytimes = self.build_daytimes(self.args.get("daytimes", DEFAULT_DAYTIMES)) # set up event listener for each sensor for sensor in self.sensors["motion"]: # listen to xiaomi sensors by default if not any([self.states["motion_on"], self.states["motion_off"]]): self.listen_event( self.motion_event, event=EVENT_MOTION_XIAOMI, entity_id=sensor ) self.refresh_timer() # do not use listen event and listen state below together continue # on/off-only sensors without events on every motion if all([self.states["motion_on"], self.states["motion_off"]]): self.listen_state( self.motion_detected, entity=sensor, new=self.states["motion_on"] ) self.listen_state( self.motion_cleared, entity=sensor, new=self.states["motion_off"] ) # display settings self.args.setdefault("listeners", self.sensors["motion"]) self.args.setdefault( "sensors_illuminance", list(self.sensors["illuminance"]) ) if self.sensors["illuminance"] else None self.args.setdefault( "sensors_humidity", list(self.sensors["humidity"]) ) if self.sensors["humidity"] else None self.args["daytimes"] = daytimes # init adutils # self.adu = ADutils( # APP_NAME, self.args, icon=APP_ICON, ad=self, show_config=True # ) self.adu.show_info(self.args) def switch_daytime(self, kwargs: Dict[str, Any]) -> None: """Set new light settings according to daytime.""" daytime = kwargs.get("daytime") if daytime is not None: self.active = daytime if not kwargs.get("initial"): delay = daytime["delay"] light_setting = daytime["light_setting"] if isinstance(light_setting, str): is_scene = True # if its a ha scene, remove the "scene." part if "." in light_setting: light_setting = (light_setting.split("."))[1] else: is_scene = False self.adu.log( f"set {hl(self.room.capitalize())} to {hl(daytime['daytime'])} → " f"{'scene' if is_scene else 'brightness'}: {hl(light_setting)}" f"{'' if is_scene else '%'}, delay: {hl(delay)}sec", icon=DAYTIME_SWITCH_ICON, ) ########################## twu: additional features ############################################# def eval_disable_switch_conf(self, confLine): # map(str.strip, my_list) conf = confLine.split(';') # split each conf elements entity, disableStat = map(str.strip, conf[0].split(',')) # get entity and disableState rtn = False # True: to disable # 1. check the status # if status is not true: no need to check att # print ('State to satisfy: {} => {}, curr: {}'.format(entity, disableStat, self.get_state(entity))) # for attConf in conf[1:]: # att, attStat = map(str.strip, attConf.split(',')) # # debug message # print ('Attribute to disable auto: {}: {} => {}, curr: {}'.format(entity, att, attStat, self.get_state(entity, att))) # print (' ==> {} att len = {}'.format(self.get_state(entity), len(conf[1:])) ) if self.get_state(entity) == disableStat: # else go through the attributes if any statCond = True attCond = False if (len(conf[1:]) == 0): attCond = True for attConf in conf[1:]: att, attStat = map(str.strip, attConf.split(',')) currAtt = self.get_state(entity, att) if (currAtt == attStat or (currAtt is None and "None" == attStat)): print('attributes satisfied to disable automation') attCond = True break if (attCond and statCond): rtn = True else: rtn = False # print ('return val of eval_disable_switch_conf {} => {} '.format(confLine, rtn)) return rtn async def fade(self, entity, direction, targetBrightnessPct, duration): # bound = self.active["light_setting"] if direction == "up" else 0 #target brightness in setting targetBrightness = targetBrightnessPct * 255 / 100 adjFrequency = 0.2 adjPoints = duration / adjFrequency initBrightness = await self.get_state(entity, "brightness") initBrightness = initBrightness if initBrightness is not None else 0 step = int(math.ceil ((float(targetBrightness) - float(initBrightness) if direction == "up" else -1.0 * float(initBrightness)) / float(adjPoints) ) ) # using int division # print (initBrightness, targetBrightness, step, direction, duration) # print (type(initBrightness), type(targetBrightness), type(step)) if (step != 0): for b in range(int(initBrightness), int(targetBrightness), int(step)): await self.call_service( "homeassistant/turn_on", entity_id = entity, brightness = b, ) t.sleep(adjFrequency) await self.call_service( "homeassistant/turn_on", entity_id = entity, brightness = int(targetBrightness), ) # self.adu.log( # f"{hl(self.room.capitalize())} turned {hl(f'on')} → " # f"brightness: {hl(self.active['light_setting'])}%", # icon=ON_ICON, #) ########################## twu: end of additional ############################################# def motion_cleared( self, entity: str, attribute: str, old: str, new: str, kwargs: Dict[str, Any] ) -> None: # starte the timer if motion is cleared if all( [ self.get_state(sensor) == self.states["motion_off"] for sensor in self.sensors["motion"] ] ): # all motion sensors off, starting timer self.refresh_timer() else: if self._handle: # cancelling active timer self.cancel_timer(self._handle) def motion_detected( self, entity: str, attribute: str, old: str, new: str, kwargs: Dict[str, Any] ) -> None: # wrapper function if self._handle: # cancelling active timer self.cancel_timer(self._handle) # calling motion event handler data: Dict[str, Any] = {"entity_id": entity, "new": new, "old": old} self.motion_event("state_changed_detection", data, kwargs) def motion_event( self, event: str, data: Dict[str, str], kwargs: Dict[str, Any] ) -> None: """Handle motion events.""" self.adu.log( f"received '{event}' event from " f"'{data['entity_id'].replace(KEYWORD_MOTION, '')}'", level="DEBUG", ) # check if automoli is disabled via home assistant entity for lineStr in self.disable_switch_entity: if self.eval_disable_switch_conf(lineStr): self.adu.log(f"AutoMoLi disabled via {lineStr}",) return # if self.get_state(self.disable_switch_entity) == "off": # self.adu.log(f"AutoMoLi disabled via {self.disable_switch_entity}",) # return # turn on the lights if not already if not any([self.get_state(light) == "on" for light in self.lights]): self.lights_on() else: self.adu.log( f"light in {self.room.capitalize()} already on → refreshing the timer", level="DEBUG", ) if event != "state_changed_detection": self.refresh_timer() def refresh_timer(self) -> None: """Refresh delay timer.""" self.cancel_timer(self._handle) if self.active["delay"] != 0: self._handle = self.run_in(self.lights_off, self.active["delay"]) def lights_on(self) -> None: """Turn on the lights.""" if self.thresholds["illuminance"]: blocker = [] for sensor in self.sensors["illuminance"]: try: if float(self.get_state(sensor)) >= self.thresholds["illuminance"]: blocker.append(sensor) except ValueError as error: self.adu.log( f"could not parse illuminance '{self.get_state(sensor)}' from " f"'{sensor}': {error}" ) return if blocker: self.adu.log( f"According to {hl(' '.join(blocker))} its already bright enough" ) return if isinstance(self.active["light_setting"], str): for entity in self.lights: if self.active["is_hue_group"] and self.get_state( entity_id=entity, attribute="is_hue_group" ): self.call_service( "hue/hue_activate_scene", group_name=self.friendly_name(entity), scene_name=self.active["light_setting"], ) continue item = entity if self.active["light_setting"].startswith("scene."): item = self.active["light_setting"] # self.turn_on(item) self.call_service("homeassistant/turn_on", entity_id=item) self.adu.log( f"{hl(self.room.capitalize())} turned {hl(f'on')} → " f"{'hue' if self.active['is_hue_group'] else 'ha'} scene: " f"{hl(self.active['light_setting'].replace('scene.', ''))}", icon=ON_ICON, ) elif isinstance(self.active["light_setting"], int): if self.active["light_setting"] == 0: self.lights_off(dict()) else: for entity in self.lights: if entity.startswith("switch."): self.call_service("homeassistant/turn_on", entity_id=entity) else: ####################################### call fade on # self.call_service( # "homeassistant/turn_on", # entity_id=entity, # brightness_pct=self.active["light_setting"], # ) # entity, direction, targetBrightnessPct, duration targetBrightnessPct = self.active['light_setting'] fadeDuration = self.fadeSetting["on"] self.create_task(self.fade(entity, "up", targetBrightnessPct, fadeDuration)) self.adu.log( f"{hl(self.room.capitalize())} turned {hl(f'on')} → " f"brightness: {hl(self.active['light_setting'])}%", icon=ON_ICON, ) else: raise ValueError( f"invalid brightness/scene: {self.active['light_setting']!s} " f"in {self.room}" ) def lights_off(self, kwargs: Dict[str, Any]) -> None: """Turn off the lights.""" # check if automoli is disabled via home assistant entity for lineStr in self.disable_switch_entity: if self.eval_disable_switch_conf(lineStr): self.adu.log(f"AutoMoLi disabled via {lineStr}",) return # if self.get_state(self.disable_switch_entity) == "off": # self.adu.log(f"AutoMoLi disabled via {self.disable_switch_entity}",) # return blocker: Any = None if self.thresholds["humidity"]: blocker = [ sensor for sensor in self.sensors["humidity"] if float(self.get_state(sensor)) >= self.thresholds["humidity"] ] blocker = blocker.pop() if blocker else None # turn off if not blocked if blocker: self.refresh_timer() self.adu.log( f"🛁 no motion in {hl(self.room.capitalize())} since " f"{hl(self.active['delay'])}s → " f"but {hl(float(self.get_state(blocker)))}%RH > " f"{self.thresholds['humidity']}%RH" ) else: self.cancel_timer(self._handle) if any([(self.get_state(entity)) == "on" for entity in self.lights]): for entity in self.lights: # self.turn_off(entity) ################## call fade self.create_task(self.fade(entity, "down", 0, self.fadeSetting["off"])) self.adu.log( f"no motion in {hl(self.room.capitalize())} since " f"{hl(self.active['delay'])}s → turned {hl(f'off')}", icon=OFF_ICON, ) def find_sensors(self, keyword: str) -> List[str]: """Find sensors by looking for a keyword in the friendly_name.""" return [ sensor for sensor in self.get_state() if keyword in sensor and self.room in (self.friendly_name(sensor)).lower().replace("ü", "u") ] def build_daytimes( self, daytimes: List[Any] ) -> Optional[List[Dict[str, Union[int, str]]]]: starttimes: Set[time] = set() delay = int(self.args.get("delay", DEFAULT_DELAY)) for idx, daytime in enumerate(daytimes): dt_name = daytime.get("name", f"{DEFAULT_NAME}_{idx}") dt_delay = daytime.get("delay", delay) dt_light_setting = daytime.get("light", DEFAULT_LIGHT_SETTING) dt_is_hue_group = ( isinstance(dt_light_setting, str) and not dt_light_setting.startswith("scene.") and any( [ self.get_state(entity_id=entity, attribute="is_hue_group") for entity in self.lights ] ) ) dt_start: time try: # dt_start = time.fromisoformat(str(daytime.get("starttime"))) dt_start = self.parse_time(daytime.get("starttime"), aware=True) print (f'{daytime.get("starttime")} => start time = {dt_start}') except ValueError as error: raise ValueError(f"missing start time in daytime '{dt_name}': {error}") # configuration for this daytime daytime = dict( daytime=dt_name, delay=dt_delay, starttime=dt_start.isoformat(), # datetime is not serializable light_setting=dt_light_setting, is_hue_group=dt_is_hue_group, ) # info about next daytime # next_dt_start = time.fromisoformat( # str(daytimes[(idx + 1) % len(daytimes)].get("starttime")) # ) next_dt_start = daytimes[(idx + 1) % len(daytimes)].get("starttime") # collect all start times for sanity check if dt_start in starttimes: raise ValueError( f"Start times of all daytimes have to be unique! " f"Duplicate found: {dt_start}", ) starttimes.add(dt_start) # check if this daytime should ne active now if self.now_is_between(str(dt_start), str(next_dt_start)): self.switch_daytime(dict(daytime=daytime, initial=True)) self.args["active_daytime"] = daytime.get("daytime") # schedule callbacks for daytime switching self.run_daily( self.switch_daytime, dt_start, random_start=-10, **dict(daytime=daytime) ) return daytimes
def initialize(self) -> None: """Initialize a room with AutoMoLi.""" self.room = str(self.args.get("room")) # self.delay = int(self.args.get("delay", DEFAULT_DELAY)) delay = int(self.args.get("delay", DEFAULT_DELAY)) self.event_motion = self.args.get("motion_event", None) self.motion_state_on = self.args.get("motion_state_on", None) self.motion_state_off = self.args.get("motion_state_off", None) # devices self.lights: Set[str] = self.args.get("lights", set()) self.sensors_motion: Set[str] = self.args.pop("motion", set()) self.sensors_illuminance: Set[str] = self.args.pop( "illuminance", set()) self.sensors_humidity: Set[str] = self.args.pop("humidity", set()) # device config self.illuminance_threshold: Optional[int] = self.args.get( "illuminance_threshold") self.humidity_threshold: Optional[int] = self.args.get( "humidity_threshold") # on/off switch via input.boolean self.disable_switch_entity = self.args.get("disable_switch_entity") # use user-defined daytimes if available daytimes: List[Dict[str, Union[int, str]]] = self.args.get( "daytimes", DEFAULT_DAYTIMES) # currently active daytime settings self.active: Dict[str, Union[int, str]] = {} self._handle = None # define light entities switched by automoli if self.lights: self.lights = set(self.lights) elif self.entity_exists(f"light.{self.room}"): self.lights.update([f"light.{self.room}"]) else: self.lights.update(self.find_sensors(KEYWORD_LIGHTS)) # define sensor entities monitored by automoli if not self.sensors_motion: self.sensors_motion.update(self.find_sensors(KEYWORD_SENSORS)) # enumerate humidity sensors if threshold given if self.humidity_threshold: if not self.sensors_humidity: self.sensors_humidity.update( self.find_sensors(KEYWORD_SENSORS_HUMIDITY)) if not self.sensors_humidity: self.log( f"No humidity sensors given/found ('{KEYWORD_SENSORS_HUMIDITY}') ", f"→ disabling humidity threshold blocker.", ) self.humidity_threshold = None # enumerate illuminance sensors if threshold given if self.illuminance_threshold: if not self.sensors_illuminance: self.sensors_illuminance.update( self.find_sensors(KEYWORD_SENSORS_ILLUMINANCE)) if not self.sensors_illuminance: self.log( f"No illuminance sensors given/found ", f"('{KEYWORD_SENSORS_ILLUMINANCE}') → ", f"disabling illuminance threshold blocker.", ) self.illuminance_threshold = None # sanity check if not self.sensors_motion: raise ValueError( f"No sensors given/found, sorry! ('{KEYWORD_SENSORS}')") elif not self.lights: raise ValueError( f"No sensors given/found, sorry! ('{KEYWORD_LIGHTS}')") starttimes: Set[time] = set() # build daytimes dict for idx, daytime in enumerate(daytimes): dt_name = daytime.get("name", f"{DEFAULT_NAME}_{idx}") dt_delay = daytime.get("delay", delay) dt_light_setting = daytime.get("light", DEFAULT_LIGHT_SETTING) dt_is_hue_group = (isinstance(dt_light_setting, str) and not dt_light_setting.startswith("scene.") and any([ self.get_state(entity_id=entity, attribute="is_hue_group") for entity in self.lights ])) py37_or_higher = sys.version_info.major >= 3 and sys.version_info.minor >= 7 dt_start: time try: if py37_or_higher: dt_start = time.fromisoformat(str( daytime.get("starttime"))) else: dt_start = self.datetime.strptime( str(daytime.get("starttime")), "%H:%M").time() except ValueError as error: raise ValueError( f"missing start time in daytime '{dt_name}': {error}") # configuration for this daytime daytime = dict( daytime=dt_name, delay=dt_delay, # starttime=dt_start, # datetime is not serializable starttime=dt_start.isoformat(), light_setting=dt_light_setting, is_hue_group=dt_is_hue_group, ) # info about next daytime if py37_or_higher: next_dt_start = time.fromisoformat( str(daytimes[(idx + 1) % len(daytimes)].get("starttime"))) else: next_dt_start = self.datetime.strptime( str(daytimes[(idx + 1) % len(daytimes)].get("starttime")), "%H:%M").time() # collect all start times for sanity check if dt_start in starttimes: raise ValueError( f"Start times of all daytimes have to be unique! ", f"Duplicate found: {dt_start}", ) starttimes.add(dt_start) # check if this daytime should ne active now if self.now_is_between(str(dt_start), str(next_dt_start)): self.switch_daytime(dict(daytime=daytime, initial=True)) self.args["active_daytime"] = daytime.get("daytime") # schedule callbacks for daytime switching self.run_daily(self.switch_daytime, dt_start, random_start=-10, **dict(daytime=daytime)) # set up event listener for each sensor for sensor in self.sensors_motion: # listen to xiaomi sensors by default if not any([self.motion_state_on, self.motion_state_off]): self.listen_event(self.motion_event, event=EVENT_MOTION_XIAOMI, entity_id=sensor) self.refresh_timer() # do not use listen event and listen state below together continue # on/off-only sensors without events on every motion if self.motion_state_on and self.motion_state_off: self.listen_state(self.motion_detected_state, entity=sensor, new=self.motion_state_on) self.listen_state(self.motion_cleared_state, entity=sensor, new=self.motion_state_off) # listen for non-xiaomi events if self.event_motion: self.log( f"\nPlease update your config to use `motion_state_on/off'\n" ) # display settings self.args.setdefault("listeners", self.sensors_motion) self.args.setdefault("sensors_illuminance", list(self.sensors_illuminance) ) if self.sensors_illuminance else None self.args.setdefault("sensors_humidity", list( self.sensors_humidity)) if self.sensors_humidity else None self.args["daytimes"] = daytimes # init adutils self.adu = ADutils(APP_NAME, self.args, icon=APP_ICON, ad=self, show_config=True)
class AutoMoLi(hass.Hass): # type: ignore """Automatic Motion Lights.""" def initialize(self) -> None: """Initialize a room with AutoMoLi.""" self.room = str(self.args.get("room")) # self.delay = int(self.args.get("delay", DEFAULT_DELAY)) delay = int(self.args.get("delay", DEFAULT_DELAY)) self.event_motion = self.args.get("motion_event", None) self.motion_state_on = self.args.get("motion_state_on", None) self.motion_state_off = self.args.get("motion_state_off", None) # devices self.lights: Set[str] = self.args.get("lights", set()) self.sensors_motion: Set[str] = self.args.pop("motion", set()) self.sensors_illuminance: Set[str] = self.args.pop( "illuminance", set()) self.sensors_humidity: Set[str] = self.args.pop("humidity", set()) # device config self.illuminance_threshold: Optional[int] = self.args.get( "illuminance_threshold") self.humidity_threshold: Optional[int] = self.args.get( "humidity_threshold") # on/off switch via input.boolean self.disable_switch_entity = self.args.get("disable_switch_entity") # use user-defined daytimes if available daytimes: List[Dict[str, Union[int, str]]] = self.args.get( "daytimes", DEFAULT_DAYTIMES) # currently active daytime settings self.active: Dict[str, Union[int, str]] = {} self._handle = None # define light entities switched by automoli if self.lights: self.lights = set(self.lights) elif self.entity_exists(f"light.{self.room}"): self.lights.update([f"light.{self.room}"]) else: self.lights.update(self.find_sensors(KEYWORD_LIGHTS)) # define sensor entities monitored by automoli if not self.sensors_motion: self.sensors_motion.update(self.find_sensors(KEYWORD_SENSORS)) # enumerate humidity sensors if threshold given if self.humidity_threshold: if not self.sensors_humidity: self.sensors_humidity.update( self.find_sensors(KEYWORD_SENSORS_HUMIDITY)) if not self.sensors_humidity: self.log( f"No humidity sensors given/found ('{KEYWORD_SENSORS_HUMIDITY}') ", f"→ disabling humidity threshold blocker.", ) self.humidity_threshold = None # enumerate illuminance sensors if threshold given if self.illuminance_threshold: if not self.sensors_illuminance: self.sensors_illuminance.update( self.find_sensors(KEYWORD_SENSORS_ILLUMINANCE)) if not self.sensors_illuminance: self.log( f"No illuminance sensors given/found ", f"('{KEYWORD_SENSORS_ILLUMINANCE}') → ", f"disabling illuminance threshold blocker.", ) self.illuminance_threshold = None # sanity check if not self.sensors_motion: raise ValueError( f"No sensors given/found, sorry! ('{KEYWORD_SENSORS}')") elif not self.lights: raise ValueError( f"No sensors given/found, sorry! ('{KEYWORD_LIGHTS}')") starttimes: Set[time] = set() # build daytimes dict for idx, daytime in enumerate(daytimes): dt_name = daytime.get("name", f"{DEFAULT_NAME}_{idx}") dt_delay = daytime.get("delay", delay) dt_light_setting = daytime.get("light", DEFAULT_LIGHT_SETTING) dt_is_hue_group = (isinstance(dt_light_setting, str) and not dt_light_setting.startswith("scene.") and any([ self.get_state(entity_id=entity, attribute="is_hue_group") for entity in self.lights ])) py37_or_higher = sys.version_info.major >= 3 and sys.version_info.minor >= 7 dt_start: time try: if py37_or_higher: dt_start = time.fromisoformat(str( daytime.get("starttime"))) else: dt_start = self.datetime.strptime( str(daytime.get("starttime")), "%H:%M").time() except ValueError as error: raise ValueError( f"missing start time in daytime '{dt_name}': {error}") # configuration for this daytime daytime = dict( daytime=dt_name, delay=dt_delay, # starttime=dt_start, # datetime is not serializable starttime=dt_start.isoformat(), light_setting=dt_light_setting, is_hue_group=dt_is_hue_group, ) # info about next daytime if py37_or_higher: next_dt_start = time.fromisoformat( str(daytimes[(idx + 1) % len(daytimes)].get("starttime"))) else: next_dt_start = self.datetime.strptime( str(daytimes[(idx + 1) % len(daytimes)].get("starttime")), "%H:%M").time() # collect all start times for sanity check if dt_start in starttimes: raise ValueError( f"Start times of all daytimes have to be unique! ", f"Duplicate found: {dt_start}", ) starttimes.add(dt_start) # check if this daytime should ne active now if self.now_is_between(str(dt_start), str(next_dt_start)): self.switch_daytime(dict(daytime=daytime, initial=True)) self.args["active_daytime"] = daytime.get("daytime") # schedule callbacks for daytime switching self.run_daily(self.switch_daytime, dt_start, random_start=-10, **dict(daytime=daytime)) # set up event listener for each sensor for sensor in self.sensors_motion: # listen to xiaomi sensors by default if not any([self.motion_state_on, self.motion_state_off]): self.listen_event(self.motion_event, event=EVENT_MOTION_XIAOMI, entity_id=sensor) self.refresh_timer() # do not use listen event and listen state below together continue # on/off-only sensors without events on every motion if self.motion_state_on and self.motion_state_off: self.listen_state(self.motion_detected_state, entity=sensor, new=self.motion_state_on) self.listen_state(self.motion_cleared_state, entity=sensor, new=self.motion_state_off) # listen for non-xiaomi events if self.event_motion: self.log( f"\nPlease update your config to use `motion_state_on/off'\n" ) # display settings self.args.setdefault("listeners", self.sensors_motion) self.args.setdefault("sensors_illuminance", list(self.sensors_illuminance) ) if self.sensors_illuminance else None self.args.setdefault("sensors_humidity", list( self.sensors_humidity)) if self.sensors_humidity else None self.args["daytimes"] = daytimes # init adutils self.adu = ADutils(APP_NAME, self.args, icon=APP_ICON, ad=self, show_config=True) def switch_daytime(self, kwargs: Dict[str, Any]) -> None: """Set new light settings according to daytime.""" daytime = kwargs.get("daytime") if daytime is not None: self.active = daytime if not kwargs.get("initial"): delay = daytime["delay"] light_setting = daytime["light_setting"] if isinstance(light_setting, str): is_scene = True # if its a ha scene, remove the "scene." part if "." in light_setting: light_setting = (light_setting.split("."))[1] else: is_scene = False self.adu.log( f"set {hl(self.room.capitalize())} to {hl(daytime['daytime'])} → " f"{'scene' if is_scene else 'brightness'}: {hl(light_setting)}" f"{'' if is_scene else '%'}, delay: {hl(delay)}sec", icon=DAYTIME_SWITCH_ICON, ) def motion_cleared_state(self, entity: str, attribute: str, old: str, new: str, kwargs: Dict[str, Any]) -> None: # starte the timer if motion is cleared if all([ self.get_state(sensor) == self.motion_state_off for sensor in self.sensors_motion ]): # all motion sensors off, starting timer self.refresh_timer() else: if self._handle: # cancelling active timer self.cancel_timer(self._handle) def motion_detected_state(self, entity: str, attribute: str, old: str, new: str, kwargs: Dict[str, Any]) -> None: # wrapper function if self._handle: # cancelling active timer self.cancel_timer(self._handle) # calling motion event handler data: Dict[str, Any] = {"entity_id": entity, "new": new, "old": old} self.motion_event("state_changed_detection", data, kwargs) def motion_event(self, event: str, data: Dict[str, str], kwargs: Dict[str, Any]) -> None: """Handle motion events.""" self.adu.log( f"received '{event}' event from " f"'{data['entity_id'].replace(KEYWORD_SENSORS, '')}'", level="DEBUG", ) # check if automoli is disabled via home assistant entity automoli_state = self.get_state(self.disable_switch_entity) if automoli_state == "off": self.adu.log( f"automoli is disabled via {self.disable_switch_entity} ", f"(state: {automoli_state})'", ) return # turn on the lights if not already if not any([self.get_state(light) == "on" for light in self.lights]): self.lights_on() else: self.adu.log( f"light in {self.room.capitalize()} already on → ", f"refreshing the timer", level="DEBUG", ) if event != "state_changed_detection": self.refresh_timer() def refresh_timer(self) -> None: """Refresh delay timer.""" self.cancel_timer(self._handle) if self.active["delay"] != 0: self._handle = self.run_in(self.lights_off, self.active["delay"]) # if self.delay != 0: # self._handle = self.run_in(self.lights_off, self.delay) def lights_on(self) -> None: """Turn on the lights.""" if self.illuminance_threshold: blocker = [ sensor for sensor in self.sensors_illuminance if float(self.get_state(sensor)) >= self.illuminance_threshold ] if blocker: self.adu.log( f"According to {hl(' '.join(blocker))} its already bright enough" ) return if isinstance(self.active["light_setting"], str): for entity in self.lights: if entity.startswith("switch."): self.turn_on(entity) if self.active["is_hue_group"] and self.get_state( entity_id=entity, attribute="is_hue_group"): self.call_service( "hue/hue_activate_scene", group_name=self.friendly_name(entity), scene_name=self.active["light_setting"], ) continue item = entity if self.active["light_setting"].startswith("scene."): item = self.active["light_setting"] self.turn_on(item) self.adu.log( f"{hl(self.room.capitalize())} turned {hl(f'on')} → " f"{'hue' if self.active['is_hue_group'] else 'ha'} scene: " f"{hl(self.active['light_setting'].replace('scene.', ''))}", icon=ON_ICON, ) elif isinstance(self.active["light_setting"], int): if self.active["light_setting"] == 0: self.lights_off(dict()) else: for entity in self.lights: if entity.startswith("switch."): self.turn_on(entity) else: self.turn_on( entity, brightness_pct=self.active["light_setting"]) self.adu.log( f"{hl(self.room.capitalize())} turned {hl(f'on')} → " f"brightness: {hl(self.active['light_setting'])}%", icon=ON_ICON, ) else: raise ValueError( f"invalid brightness/scene: {self.active['light_setting']!s} " f"in {self.room}") def lights_off(self, kwargs: Dict[str, Any]) -> None: """Turn off the lights.""" blocker: Any = None if self.humidity_threshold: blocker = [ sensor for sensor in self.sensors_humidity if float(self.get_state(sensor)) >= self.humidity_threshold ] blocker = blocker.pop() if blocker else None # turn off if not blocked if blocker: self.refresh_timer() self.adu.log(f"🛁 no motion in {hl(self.room.capitalize())} since " f"{hl(self.active['delay'])}s → " f"but {hl(float(self.get_state(blocker)))}%RH > " f"{self.humidity_threshold}%RH") else: self.cancel_timer(self._handle) if any([(self.get_state(entity)) == "on" for entity in self.lights]): for entity in self.lights: self.turn_off(entity) self.adu.log( f"no motion in {hl(self.room.capitalize())} since " f"{hl(self.active['delay'])}s → turned {hl(f'off')}", icon=OFF_ICON, ) def find_sensors(self, keyword: str) -> List[str]: """Find sensors by looking for a keyword in the friendly_name.""" return [ sensor for sensor in self.get_state() if keyword in sensor and self.room in ( self.friendly_name(sensor)).lower().replace("ü", "u") ]