class ToggleAtTime(SwitchBase): """Define a feature to take action on certain time.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema({vol.Required(CONF_SWITCH): vol_help.entity_id}, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_ACTION_TYPE): vol.In(ACTION_TYPES), vol.Required(CONF_STATE): str, vol.Required(CONF_SCHEDULE_TIME): str, vol.Optional(CONF_PARAMETERS): dict, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.switch = self.entities[CONF_SWITCH] self.action_type = self.properties[CONF_ACTION_TYPE] self.state = self.properties[CONF_STATE] self.parameters = self.properties.get(CONF_PARAMETERS, {}) self.run_daily( self.action_on_schedule, self.parse_time(self.properties[CONF_SCHEDULE_TIME]), action_type=self.action_type, state=self.state, action_entity=self.switch, parameters=self.parameters, constrain_app_enabled=1, )
class LastMotion(AppBase): """Define a feature to update a sensor with the name of the room where last motion was detected""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_MOTION_SENSORS): vol.Schema([vol.Optional(vol_help.entity_id)]) }, extra=vol.ALLOW_EXTRA, ) }) def configure(self): """Configure.""" for sensor in self.entities[CONF_MOTION_SENSORS]: self.listen_state(self.motion_detected, sensor, new="on") def motion_detected(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Select the room input select based on the triggered entity.""" room_name = entity.split(".")[1].split("_", 1)[-1].capitalize() self.select_option(HOUSE["last_motion"], room_name)
class NotifyOnHighHumidity(AppBase): """Define a feature to notify on high humidity.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({vol.Required(CONF_CHECK_INTERVAL): int}, extra=vol.ALLOW_EXTRA), CONF_NOTIFICATIONS: vol.Schema({vol.Required(CONF_TARGETS): str}), }) def configure(self): """Configure.""" self.run_every( self.send_notification, datetime.datetime.now(), self.properties[CONF_CHECK_INTERVAL] * 60, constrain_app_enabled=1, ) def send_notification(self, kwargs: dict) -> None: """Send notification on high humidity.""" if self.climate_app.humidity_in_room_high: self.notification_app.notify( kind=SINGLE, level=HOME, title="Hohe Luftfeuchtigkeit!", message= f"Im {', '.join(self.climate_app.which_room_high_humidity)} " f"herrscht eine hohe Luftfeuchtigkeit", targets=self.notifications[CONF_TARGETS], )
class NotifyOnCleaningDay(AppBase): """Define a feature to send a notification in morning of the cleaning day.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema( {vol.Optional(CONF_REMINDER_TIME): vol_help.valid_time}, extra=vol.ALLOW_EXTRA, ), CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.run_daily( self.notify_on_cleaning_day, self.parse_time(self.properties[CONF_REMINDER_TIME]), constrain_app_enabled=1, ) def notify_on_cleaning_day(self, kwargs: dict) -> None: """Send notification in the morning to remind of cleaning day.""" self.notification_app.notify( kind="single", level="emergency", title="Putztag", message=f"Heute ist Putztag. Bitte Möbel richten!", targets=self.notifications["targets"], )
class NotifyOnBadLoginAttempt(AppBase): """Define a feature to send a notification when bad login happened.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ) }) def configure(self): """Configure.""" self.listen_state( self.bad_login_attempt, "persistent_notification.http_login", new="notifying", ) def bad_login_attempt(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notification when bad login happened.""" msg = self.get_state("persistent_notification.http_login", attribute="message") self.notification_app.notify( kind="single", level="emergency", title="Falscher Loginversuch", message=msg, targets=self.notifications["targets"], )
class WasherAutomation(AppBase): # pylint: disable=too-few-public-methods """Define a base for washer-type appliances automations.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_POWER): vol_help.entity_id, vol.Required(CONF_STATUS): vol_help.entity_id, }, extra=vol.ALLOW_EXTRA, ) }) class WasherStates(Enum): """Define an enum for Washer states.""" clean = "Sauber" dirty = "Dreckig" running = "Läuft" drying = "Trocknung" @property def washer_state(self) -> "WasherStates": """Return the current state of the washer appliance.""" return self.WasherStates(self.get_state(self.entities[CONF_STATUS])) @washer_state.setter def washer_state(self, washer_state: WasherStates) -> None: """Set the the washer appliance to given state.""" self.select_option(self.entities[CONF_STATUS], washer_state.value)
class Person(AppBase): """Representation of a Person.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required("person"): str, vol.Required("attributes"): vol.Schema({ vol.Required("full_name"): str, vol.Optional("notifiers"): vol.All(cv.ensure_list, [str]), }) }) def configure(self) -> None: """Configure a person.""" person = self.args["person"].lower() attributes = self.args["attributes"] entity_id = f"person.{person}" attributes.update({ "id": person, "home": None, "non_binary_presence": None, "sleep_state": None, }) self.adbase.set_state(entity_id, attributes=attributes)
class ToggleOnArrival(SwitchBase): """Define a feature to take action on arrival of person/everyone.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema({vol.Required(CONF_SWITCH): vol_help.entity_id}, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_ACTION_TYPE): vol.In(ACTION_TYPES), vol.Required(CONF_STATE): str, vol.Optional(CONF_SPECIFIC_PERSON): vol.In(PERSONS.keys()), vol.Optional(CONF_PARAMETERS): dict, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.switch = self.entities[CONF_SWITCH] self.action_type = self.properties[CONF_ACTION_TYPE] self.state = self.properties[CONF_STATE] self.delay = self.properties.get(CONF_DELAY) self.parameters = self.properties.get(CONF_PARAMETERS, {}) self.person = self.properties.get(CONF_SPECIFIC_PERSON) self.listen_state(self.someone_arrived, HOUSE[CONF_PRESENCE_STATE], constrain_app_enabled=1) def someone_arrived(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Take action when a person arrives.""" someone_home_states = [ self.presence_app.HouseState.someone.value, self.presence_app.HouseState.everyone.value, ] # Only take action if noone was home before if (new in someone_home_states) and (old not in someone_home_states): persons_home = self.presence_app.persons_home # Only take action if specified person arrives alone or no # person was specified if (self.person in persons_home and len(persons_home) == 1) or not self.person: if self.delay: self.run_in( self.action_on_schedule, self.delay * 60, action_type=self.action_type, state=self.state, action_entity=self.switch, parameters=self.parameters, ) else: self.action(self.action_type, self.state, self.switch, **self.parameters)
class HousePresence(AppBase): """Define a base class for house presence.""" APP_SCHEMA = APP_SCHEMA.extend({vol.Required("house_id"): str}) def configure(self) -> None: """Configure.""" house_id = self.args["house_id"] self.house_entity_id = f"house.{house_id}" persons = self.adbase.get_state("person") # Listen for person changing home state for person in persons.keys(): self.adbase.listen_state(self.on_presence_change, person, attribute="home") def on_presence_change(self, entity: str, attribute: str, old: str, new: str, kwargs: dict) -> None: """Respond when person changes presence state.""" person_id = entity.split(".")[1] persons = self.adbase.get_state("person") persons_home = self.adbase.get_state(self.house_entity_id, attribute="persons") persons_extended_away = [ person for person, attributes in persons.items() if attributes["attributes"]["non_binary_presence"] == "extended_away" ] # Add/remove person from the house if new and person_id not in persons_home: persons_home.append(person_id) elif person_id in persons_home: persons_home.remove(person_id) # Set occupancy of the house if not persons_home: occupied = False else: occupied = True # Set presence state of the house if len(persons.keys()) == len(persons_home): presence_state = "everyone_home" elif len(persons.keys()) == len(persons_extended_away): presence_state = "vacation" elif not persons_home: presence_state = "nobody_home" else: presence_state = "someone_home" self.adbase.set_state( self.house_entity_id, presence_state=presence_state, occupied=occupied, persons=persons_home, ) self.adbase.log(f"House Presence: {presence_state.replace('_',' ')}")
class ClimateAutomation(AppBase): """Define a base for climate automations.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_HUMIDITY_SENSORS): vol.Schema({str: vol_help.entity_id}), vol.Required(CONF_TEMPERATURE_SENSORS): vol.Schema({str: vol_help.entity_id}), vol.Required(CONF_WINDOW_SENSORS): vol_help.entity_id_list, }, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema({CONF_THRESHOLDS: vol.Schema({str: int})}, extra=vol.ALLOW_EXTRA), }) def configure(self): """Configure.""" self.humidity_sensors = self.entities[CONF_HUMIDITY_SENSORS] self.temp_sensors = self.entities[CONF_TEMPERATURE_SENSORS] self.thresholds = self.properties[CONF_THRESHOLDS] @property def window_is_open(self) -> bool: """Return true if a window/door is open.""" return len(self.which_window_open) != 0 @property def which_window_open(self) -> list: """Return a list of open windows/doors.""" return [ window for window in self.entities[CONF_WINDOW_SENSORS] if self.get_state(window) == "on" ] @property def humidity_in_room_high(self) -> bool: """Return true if humidity in a room is high.""" return len(self.which_room_high_humidity) != 0 @property def which_room_high_humidity(self) -> list: """Return a list of rooms with high humidity.""" return [ self.room_name(room) for room, sensor in self.humidity_sensors.items() if float(self.get_state(sensor)) > float(self.thresholds[room]) ] @staticmethod def room_name(room: str) -> str: """Return the friendly room name.""" return room.replace("_", " ").capitalize()
class NotifyWhenBinFull(AppBase): """Define a feature to send a notification when the bin is full.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema({vol.Required(VACUUM): vol_help.entity_id}, extra=vol.ALLOW_EXTRA), CONF_NOTIFICATIONS: vol.Schema( { vol.Required(CONF_TARGETS): vol.In(PERSONS.keys()), vol.Optional(CONF_INTERVAL): int, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.listen_state( self.notify_bin_full, self.vacuum_app.vacuum, attribute=BIN_FULL, old=False, new=True, constrain_app_enabled=1, ) self.listen_state( self.bin_emptied, self.vacuum_app.vacuum, attribute=BIN_FULL, old=True, new=False, constrain_app_enabled=1, ) def notify_bin_full(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send repeating notification that bin should be emptied.""" self.handles[BIN_FULL] = self.notification_app.notify( kind="repeat", level="home", title="Pedro voll!", message="Pedro muss geleert werden", targets=self.notifications["targets"], interval=self.notifications["interval"] * 60, ) self.log("Abfalleimer voll! Benachrichtige zum Leeren.") def bin_emptied(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Cancel the notification when bin has been emptied.""" if BIN_FULL in self.handles: self.handles.pop(BIN_FULL)() self.log("Abfalleimer geleert! Schalte Benachrichtigung aus")
class ToggleOnStateChange(SwitchBase): """Define a feature to take action when an entity enters a state.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_SWITCH): vol_help.entity_id, vol.Required(CONF_TARGET): vol_help.entity_id, }, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_ACTION_TYPE): vol.In(ACTION_TYPES), vol.Required(CONF_TARGET_STATE): str, vol.Required(CONF_STATE): str, vol.Optional(CONF_DELAY): int, vol.Optional(CONF_PARAMETERS): dict, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.switch = self.entities[CONF_SWITCH] self.action_type = self.properties[CONF_ACTION_TYPE] self.state = self.properties[CONF_STATE] self.delay = self.properties.get(CONF_DELAY) self.parameters = self.properties.get(CONF_PARAMETERS, {}) self.listen_state( self.state_change, self.entities[CONF_TARGET], new=self.properties[CONF_TARGET_STATE], constrain_app_enabled=1, ) def state_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Take action when entity enters target state.""" if self.delay: self.run_in( self.action_on_schedule, self.delay * 60, action_type=self.action_type, state=self.state, action_entity=self.switch, parameters=self.parameters, ) else: self.action(self.action_type, self.state, self.switch, **self.parameters)
class ToggleOnDeparture(SwitchBase): """Define a feature to take action when everyone leaves.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITIES: vol.Schema( {vol.Required(CONF_SWITCH): vol_help.entity_id}, extra=vol.ALLOW_EXTRA ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_ACTION_TYPE): vol.In(ACTION_TYPES), vol.Required(CONF_STATE): str, vol.Optional(CONF_PARAMETERS): dict, }, extra=vol.ALLOW_EXTRA, ), } ) def configure(self) -> None: """Configure.""" self.switch = self.entities[CONF_SWITCH] self.action_type = self.properties[CONF_ACTION_TYPE] self.state = self.properties[CONF_STATE] self.delay = self.properties.get(CONF_DELAY) self.parameters = self.properties.get(CONF_PARAMETERS, {}) self.listen_state( self.everyone_left, HOUSE[CONF_PRESENCE_STATE], new=self.presence_app.HouseState.noone.value, constrain_app_enabled=1, ) def everyone_left( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Take action when everyone left the house.""" if self.delay: self.run_in( self.action_on_schedule, self.delay * 60, action_type=self.action_type, state=self.state, action_entity=self.switch, parameters=self.parameters, ) else: self.action(self.action_type, self.state, self.switch, **self.parameters)
class RemoteAutomation(AppBase): """Define a base feature for remote automations.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITIES: vol.Schema( {vol.Required(CONF_HARMONY_REMOTE): vol_help.entity_id}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( {vol.Required(CONF_ACTIVITIES): dict}, extra=vol.ALLOW_EXTRA ), } ) def configure(self) -> None: """Configure.""" self.remote = self.entities[CONF_HARMONY_REMOTE] self.activities = self.properties[CONF_ACTIVITIES] @property def current_device_id(self) -> Union[int, None]: """Get device id of current activity.""" try: return self.activities[self.current_activity_name.replace(" ", "_").lower()] except KeyError: return None @property def current_activity_name(self) -> Union[str, None]: """Get the name of the current activity.""" return self.get_state(self.remote, attribute=CURRENT_ACTIVITY) @property def remote_is_off(self) -> bool: """Return the power state of the remote control.""" return self.current_device_id == -1 def send_command(self, command: str, **kwargs: dict) -> None: """Send a command to the remote.""" device_id = kwargs.get(CONF_DEVICE, self.current_device_id) self.log(device_id) self.call_service( "remote/send_command", entity_id=self.remote, device=device_id, command=command, )
class RoomPresence(AppBase): """Define a base class for room presence.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required("sensors"): vol.Schema({vol.Optional(str): cv.entity_id}) }) def configure(self) -> None: """Configure.""" room_presence_sensors = self.args["sensors"] for person, sensor in room_presence_sensors.items(): # Listen for person changing area self.hass.listen_state(self.on_sensor_change, sensor, duration=5, person_id=person) def on_sensor_change(self, entity: str, attribute: str, old: str, new: str, kwargs: dict) -> None: """Respond when room presence sensor changes state.""" person_id = kwargs["person_id"] person_entity = f"person.{person_id}" old_state = self.adbase.get_state(person_entity, attribute="area") if new != old_state: areas = self.adbase.get_state("area") area_entity = f"area.{new}" # Remove person from other areas for area in areas.keys(): if area != area_entity: persons = self.adbase.get_state(area, attribute="persons") if person_id in persons: persons.remove(person_id) self.adbase.set_state(area, persons=persons) # Add person to new area if new != "not_home": persons = self.adbase.get_state(area_entity, attribute="persons") if person_id not in persons: persons.append(person_id) self.adbase.set_state(area_entity, persons=persons) # Set area for person self.adbase.set_state(person_entity, area=new) self.adbase.log(f"{person_id.capitalize()} Area: {new}")
class House(AppBase): """Representation of a House.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required("id"): str, vol.Optional("attributes"): vol.Schema({ vol.Optional("friendly_name"): str, }) }) def configure(self) -> None: """Configure an area.""" houses = self.adbase.get_state("house") house_id = self.args["id"] attributes = self.args.get("attributes") entity_id = f"house.{house_id}" # Create an entity for the house if it doesn't already exist if entity_id not in houses.keys(): if "friendly_name" not in attributes: attributes.update({"friendly_name": house_id.title()}) attributes.update({ "id": house_id, "persons": [], "occupied": None, "presence_state": None, "sleep_state": None }) self.adbase.set_state(entity_id, state="idle", attributes=attributes) # Listen for no changes in house state for 30 seconds self.adbase.listen_state(self.state_changed, entity_id, duration=30) def state_changed(self, entity: str, attribute: dict, old: str, new: str, kwargs: dict) -> None: """Respond when house doesn't change state for 30s.""" # Set area to idle self.adbase.set_state(entity, state="idle")
class NotificationOnChange(AppBase): """Define a feature to send a notification when the alarm state changed.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ) }) def configure(self): """Configure.""" self.listen_state(self.alarm_state_changed, HOUSE[ALARM_STATE], constrain_app_enabled=1) self.listen_event( self.disarm_on_push_notification, "html5_notification.clicked", action=WRONG_ALARM, constrain_app_enabled=1, ) def alarm_state_changed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notification when alarm state changed.""" self.notification_app.notify( kind="single", level="emergency", title="Alarm Status gewechselt", message=f"Der neue Alarm Status ist {new}", targets=self.notifications["targets"], data={"actions": [{ "action": WRONG_ALARM, "title": "Fehlalarm" }]}, ) def disarm_on_push_notification(self, event_name: str, data: dict, kwargs: dict) -> None: """Disarm when push notification got clicked.""" self.security_app.alarm_state = self.security_app.AlarmType.disarmed self.log("Fehlalarm, Alarmanlage wird ausgeschaltet!")
class NotifyOnWindowOpen(AppBase): """Define a feature to notify on a window open longer than threshold.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( {vol.Required(CONF_WINDOW_SENSORS): vol_help.entity_id_list}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema({vol.Required(CONF_WINDOW_OPEN_THRESHOLD): int}, extra=vol.ALLOW_EXTRA), CONF_NOTIFICATIONS: vol.Schema({vol.Required(CONF_TARGETS): str}), }) def configure(self): """Configure.""" self.window_open_threshold = self.properties[ CONF_WINDOW_OPEN_THRESHOLD] for entity in self.entities[CONF_WINDOW_SENSORS]: self.listen_state( self.send_notification, entity, new="on", duration=self.window_open_threshold * 60, constrain_app_enabled=1, ) def send_notification(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notification if a window is open longer than threshold.""" self.notification_app.notify( kind=SINGLE, level=EMERGENCY, title="Fenster offen!", message=f"Das Fenster im " f"{entity.split('.')[1].split('_')[-1].capitalize()} " f"ist länger als {self.window_open_threshold} " f"Minuten offen.", targets=self.notifications["targets"], )
class NotifyOnNewVersion(AppBase): """Define an automation to notify when a new version is available.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_AVAILABLE): vol.Schema([vol.Optional(vol_help.entity_id)]) }, extra=vol.ALLOW_EXTRA, ), CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" for entity in self.entities[CONF_AVAILABLE]: self.listen_state(self.version_changed, entity, constrain_app_enabled=1) def version_changed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notification when new version is available.""" self.log(old) self.log(new) if new != old: app = entity.split(".")[1].split("_")[0] self.notification_app.notify( kind="single", level="home", title=f"Neue Version für {app}!", message=f"Die Version {self.get_state(entity)} " f"für {app} ist verfügbar.", targets=self.notifications["targets"], )
class NotifyOnLowBattery(AppBase): """Define an automation to notify on low battery.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_TRACKING_DEVICES): vol.Schema([vol.Optional(vol_help.entity_id)]) }, extra=vol.ALLOW_EXTRA, ), CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema({vol.Optional(CONF_BATTERY_LOW_THRESHOLD): int}, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" for entity in self.entities[CONF_TRACKING_DEVICES]: self.listen_state(self.battery_low, entity, constrain_app_enabled=1) def battery_low(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notification when battery is below threshold.""" if float(new) < float(self.properties[CONF_BATTERY_LOW_THRESHOLD]): self.notification_app.notify( kind="single", level="home", title="Batterie niedrig!", message=f"Die Batterie für {entity} ist niedrig.", targets=self.notifications["targets"], )
class MotionLightAutomation(AppBase): # pylint: disable=too-many-instance-attributes """Define a base feature for motion based lights.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITIES: vol.Schema( { vol.Required(CONF_MOTION_SENSOR): vol_help.entity_id, vol.Optional(CONF_LUX_SENSOR): vol_help.entity_id, vol.Required(CONF_LIGHTS): vol.Schema( { vol.Required(MORNING): vol_help.entity_id_list, vol.Required(DAY): vol_help.entity_id_list, vol.Required(NIGHT): vol_help.entity_id_list, } ), }, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Optional(CONF_LUX_THRESHOLD): int, vol.Optional(CONF_DELAY): int, vol.Required(CONF_DAY_STATE_TIME): vol.Schema( { vol.Required(MORNING): str, vol.Required(DAY): str, vol.Required(NIGHT): str, } ), vol.Optional(CONF_BRIGHTNESS_LEVEL): vol.Schema( {vol.Optional(vol.In(DAY_STATES)): int} ), vol.Optional(CONF_LIGHT_COLOR): vol.Schema( {vol.Optional(vol.In(DAY_STATES)): str} ), }, extra=vol.ALLOW_EXTRA, ), } ) def configure(self) -> None: """Configure.""" self.motion_sensor = self.entities[CONF_MOTION_SENSOR] self.delay = self.properties.get(CONF_DELAY, 5) * 60 self.no_action_entities = self.entities.get(CONF_NO_ACTION_ENTITIES, "") self.day_state_map = self.properties[CONF_DAY_STATE_TIME] self.brightness_map = self.properties.get(CONF_BRIGHTNESS_LEVEL, {}) self.color_map = self.properties.get(CONF_LIGHT_COLOR, {}) self.lights_map = self.entities[CONF_LIGHTS] # creates a list of a split of each element in self.lights_map self.all_lights = set() for lights in self.lights_map.values(): for light in lights.split(","): self.all_lights.add(light) self.room_name = self.motion_sensor.split(".")[1].split("_", 1)[-1].capitalize() self.listen_state( self.motion, self.motion_sensor, new=ON, constrain_app_enabled=1 ) def motion( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Take action on motion.""" self.log(f"Bewegung im {self.room_name} erkannt.") if not self.no_action_entities_on: if self.lights_on: self.log("Licht ist bereits an, Timer neustarten.") self.turn_off_delayed() elif self.lux_high: self.log("Lichtstärke ist genug hoch, keine Aktion.") else: self.turn_light_on() self.turn_off_delayed() def turn_light_on(self) -> None: """Turn lights on based on state of day.""" for entity in self.lights: self.turn_on( entity, brightness=self.brightness, color_name=self.light_color ) self.log( f"Das Licht im {self.room_name} " f"wurde durch Bewegung eingeschaltet." ) def turn_light_off(self, *args: list) -> None: """Turn lights off if none of the no action entities is on.""" if not self.no_action_entities_on: for entity in self.lights_on: self.turn_off(entity) self.log( f"Das Licht im {self.room_name} " f"wurde durch den Timer ausgeschaltet." ) def turn_off_delayed(self) -> None: """Set timer to turn light off after specified delay.""" if LIGHT_TIMER in self.handles: self.cancel_timer(self.handles[LIGHT_TIMER]) self.handles.pop(LIGHT_TIMER) self.handles[LIGHT_TIMER] = self.run_in(self.turn_light_off, self.delay) self.log( f"Ein Timer von {round(self.delay / 60)} Minuten " f"wurde im {self.room_name} eingeschaltet." ) @property def no_action_entities_on(self) -> list: """Return list of no action entities that are on""" return [ entity for entity in self.no_action_entities.split(",") if self.get_state(entity) in ON_STATES ] @property def lights_on(self) -> list: """Return list of lights that are on.""" return [entity for entity in self.all_lights if self.get_state(entity) == ON] @property def lux_high(self) -> bool: """Return true if lux in room is above threshold.""" if CONF_LUX_SENSOR in self.entities: lux_sensor = self.entities[CONF_LUX_SENSOR] lux_threshold = self.properties.get(CONF_LUX_THRESHOLD, 100) return float(self.get_state(lux_sensor)) > float(lux_threshold) return False @property def day_state(self) -> str: """Return the state of the day based on the current time.""" if self.now_is_between(self.day_state_map[MORNING], self.day_state_map[DAY]): return MORNING if self.now_is_between(self.day_state_map[DAY], self.day_state_map[NIGHT]): return DAY return NIGHT @property def lights(self) -> list: """Return list of lights to turn on based on state of day.""" return [entity for entity in self.lights_map[self.day_state].split(",")] @property def brightness(self) -> float: """Return brightness to set light to based on state of day.""" brightness_level = self.brightness_map.get(self.day_state, 75) return 255 / 100 * int(brightness_level) @property def light_color(self) -> str: """Return light color to set light to based on state of day.""" return self.color_map.get(self.day_state, "white")
class NotifyOnDeviceOffline(AppBase): """Define an automation to notify on device going offline.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_TRACKING_DEVICES): vol.Schema([vol.Optional(vol_help.entity_id)]) }, extra=vol.ALLOW_EXTRA, ), CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" for entity in self.entities[CONF_TRACKING_DEVICES]: device_type = entity.split(".")[0] if device_type == SWITCH: self.listen_state( self.device_offline, entity, new=OFF, duration=60 * 5, constrain_app_enabled=1, ) elif device_type == DEVICE_TRACKER: self.listen_state( self.device_offline, entity, new=NOT_HOME, duration=60 * 5, constrain_app_enabled=1, ) elif device_type == ZWAVE: self.listen_state( self.device_offline, entity, new=UNKNOWN, duration=60 * 5, constrain_app_enabled=1, ) self.listen_state( self.device_offline, entity, new=UNAVAILABLE, duration=60 * 5, constrain_app_enabled=1, ) elif device_type in (SENSOR, BINARY_SENSOR): self.listen_state( self.device_offline, entity, new=UNAVAILABLE, duration=60 * 5, constrain_app_enabled=1, ) def device_offline(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notification when device is offline longer than 5 minutes.""" self.notification_app.notify( kind="single", level="emergency", title="Gerät offline!", message=f"Das folgende Gerät ist offline {entity}", targets=self.notifications["targets"], )
class SwitchBase(AppBase): """Define a base class for switches.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required("switch_id"): str, vol.Optional("lights"): cv.entity_ids, vol.Optional("custom_button_config"): vol.Schema({ vol.Required(str): vol.Schema({ vol.Required("service"): str, vol.Optional("entity_id"): cv.entity_ids, vol.Optional("data"): dict, }) }), }) def configure(self) -> None: """Configure.""" switch_id = self.args["switch_id"] self.lights = self.args.get("lights") self.custom_action_map = self.args.get("custom_button_config", {}) self.action_map = {} self.button_map = {} # Check if lights are deconz lights self.check_deconz_light() # Listen for button presses self.hass.listen_event(self.on_button_press, "deconz_event", id=switch_id) def on_button_press(self, event_name: str, data: dict, kwargs: dict) -> None: """Respond on button press.""" button_name = self.get_button_name(data["event"]) action_map = {**self.action_map, **self.custom_action_map} # Check if button press is configured try: service_data = action_map[button_name] except KeyError: return # Execute light services if isinstance(service_data, str): light_service = getattr(SwitchBase, service_data) light_service(self) self.adbase.log( f"Switch executed {service_data} for {', '.join(self.lights)}") return # Execute custom button presses delay = service_data.get("delay") if delay: self.adbase.run_in(self.action_on_schedule, delay, service_data=service_data) else: self.action(service_data) def check_deconz_light(self) -> bool: """Raise error when configured lights are not deconz lights.""" if self.lights: if any( self.hass.get_state(light, attribute="is_deconz_group") is None for light in self.lights): raise ValueError( "Only DeCONZ lights can be used with the light configuration" ) def get_button_name(self, button_code) -> str: """Get the human friendly name of the button press.""" try: return self.button_map[button_code] except KeyError: return None def get_light_type(self, light: str) -> str: """Get the DeCONZ light type of the given light.""" if self.hass.get_state(light, attribute="is_deconz_group"): return "/action" return "/state" def action(self, service_data) -> None: """Execute service.""" service = service_data["service"].replace(".", "/") entity = service_data.get("entity_id") data = service_data.get("data", {}) self.hass.call_service(service, entity_id=entity, **data) self.adbase.log(f"Switch executed {service} for {', '.join(entity)}") def action_on_schedule(self, kwargs: dict) -> None: """Execute service on specified time.""" self.action(kwargs["service_data"]) def light_on(self) -> None: """Turn lights on.""" for light in self.lights: self.hass.turn_on(light) def light_on_full(self) -> None: """Turn on light at full brightness.""" for light in self.lights: self.hass.call_service("light/turn_on", entity_id=light, brightness_pct=100) def light_off(self) -> None: """Turn lights off.""" for light in self.lights: self.hass.turn_off(light) def light_toggle(self) -> None: """Toggle lights.""" for light in self.lights: self.hass.toggle(light) def light_dim_up_step(self) -> None: """Increase brightness by 10%.""" for light in self.lights: self.hass.call_service("light/turn_on", entity_id=light, brightness_step_pct=10) def light_dim_up_hold(self) -> None: """Smoothly increase brightness.""" for light in self.lights: self.hass.call_service( "deconz/configure", field=self.get_light_type(light), entity=light, data={ "bri_inc": 254, "transitiontime": 50 }, ) def light_dim_down_step(self) -> None: """Decrease brightness by 10%.""" for light in self.lights: self.hass.call_service("light/turn_on", entity_id=light, brightness_step_pct=-10) def light_dim_down_hold(self) -> None: """Smoothly decrease brightness.""" for light in self.lights: self.hass.call_service( "deconz/configure", field=self.get_light_type(light), entity=light, data={ "bri_inc": -254, "transitiontime": 50 }, ) def light_stop_dim(self) -> None: """Stop dimming.""" for light in self.lights: self.hass.call_service( "deconz/configure", field=self.get_light_type(light), entity=light, data={"bri_inc": 0}, )
class SecurityAutomation(AppBase): """Define a base for security automations.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema( { vol.Required(CONF_ALARM_LIGHTS): vol.Schema([vol.Optional(vol_help.entity_id)]), vol.Required(CONF_MOTION_SENSORS): vol.Schema([vol.Optional(vol_help.entity_id)]), vol.Required(CONF_DOOR_SENSORS): vol.Schema([vol.Optional(vol_help.entity_id)]), }, extra=vol.ALLOW_EXTRA, ) }) class AlarmType(Enum): """Define an enum for Alarm types.""" armed_no_motion = "Scharf ohne Bewegung" armed_motion = "Scharf mit Bewegung" disarmed = "Ungesichert" alert = "Einbrecher" def configure(self) -> None: """Configure.""" self.alarm_lights = self.entities[CONF_ALARM_LIGHTS] self.motion_sensors = self.entities[CONF_MOTION_SENSORS] self.door_sensors = self.entities[CONF_DOOR_SENSORS] self.code = self.args["code"] for entity in self.motion_sensors: self.listen_state(self.motion_triggered, entity, new=ON, constrain_app_enabled=1) for entity in self.door_sensors: self.listen_state(self.door_opened, entity, new=ON, constrain_app_enabled=1) @property def alarm_state(self) -> "AlarmType": """Return the current state of the security system.""" return self.AlarmType(self.get_state(HOUSE[ALARM_STATE])) @alarm_state.setter def alarm_state(self, alarm_state: AlarmType) -> None: """Set the the security system to given state.""" self.select_option(HOUSE[ALARM_STATE], alarm_state.value) if alarm_state == self.AlarmType.armed_motion: self.call_service("alarm_control_panel/alarm_arm_away", entity_id=HOUSE["alarm_panel"], code=self.code) elif alarm_state == self.AlarmType.armed_no_motion: self.call_service("alarm_control_panel/alarm_arm_home", entity_id=HOUSE["alarm_panel"], code=self.code) elif alarm_state == self.AlarmType.disarmed: self.call_service("alarm_control_panel/alarm_disarm", entity_id=HOUSE["alarm_panel"], code=self.code) elif alarm_state == self.AlarmType.alert: self.call_service("alarm_control_panel/alarm_trigger", entity_id=HOUSE["alarm_panel"], code=self.code) def motion_triggered(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Take action when motion sensor is triggered based on alarm state.""" if self.alarm_state == self.AlarmType.armed_motion: self.alarm_state = self.AlarmType.alert self.log(f"Bewegung im " f"{entity.split('.')[1].split('_')[1].capitalize()}!!!") def door_opened(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Take action when a door is opened based on alarm state.""" if self.alarm_state in ( self.AlarmType.armed_motion, self.AlarmType.armed_no_motion, ): self.alarm_state = self.AlarmType.alert self.log(f"{entity.split('.')[1].split('_')[0].capitalize()}" f" im/in der " f"{entity.split('.')[1].split('_')[1].capitalize()}" f" wurde geöffnet!!!") # test presence first # need a way to cancel flash lights # if new == 'on' or new == 'offen': # self.log("Lichter werden jetzt blinken!") # for light in self.alarm_lights: # self.turn_on(light, brightness=255, color_name='white') # self.flash_lights(light) def flash_lights(self, light: str) -> None: """Flash lights as long as alarm state is alert.""" self.toggle(light) if self.self.alarm_state == self.AlarmType.alert: self.run_in(self.flash_lights(light), 1)
class SceneLights(AppBase): """Define a feature to change light based on the current activity.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITIES: vol.Schema( {vol.Required(CONF_LIGHTS): vol_help.entity_id_list}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_SCENE_COLOR): str, vol.Required(CONF_SCENE_BRIGHTNESS): str, vol.Optional(CONF_SCENE_COLOR): dict, vol.Optional(CONF_SCENE_BRIGHTNESS): dict, }, extra=vol.ALLOW_EXTRA, ), } ) def configure(self) -> None: """Configure.""" self.lights = self.entities[CONF_LIGHTS].split(",") self.scene_color_map = self.properties[CONF_SCENE_COLOR] self.scene_brightness_map = self.properties[CONF_SCENE_BRIGHTNESS] self.transition_on = self.properties.get(CONF_TRANSITION_ON, 2) self.transition_off = self.properties.get(CONF_TRANSITION_OFF, 60) self.listen_state( self.scene_changed, self.remote_app.remote, attribute=CURRENT_ACTIVITY, constrain_app_enabled=1, ) def scene_changed( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Change the light when the acitivity changed.""" if self.scene_name(new) == POWER_OFF: for light in self.lights: self.turn_off(light, transition=self.transition_off) else: for light in self.lights: self.turn_on( light, brightness=self.brightness(new), color_name=self.light_color(new), transition=self.transition_on, ) def brightness(self, scene: str) -> float: """Get the specified brightness for the given scene.""" brightness_pct = self.scene_brightness_map.get(self.scene_name(scene), 75) return 255 / 100 * int(brightness_pct) def light_color(self, scene: str) -> str: """Get the specified light color for the given scene.""" return self.scene_color_map.get(self.scene_name(scene), "white") @staticmethod def scene_name(scene: str) -> str: """Convert the scene name to the correct format.""" return scene.replace(" ", "_").lower() def brighten_lights(self): """Brighten lights.""" for light in self.lights: self.turn_on(light, brightness=200, color_name="white", transition=2) def dim_lights(self): """Dim lights.""" current_activity = self.remote_app.current_activity_name for light in self.lights: self.turn_on( light, brightness=self.brightness(current_activity), color_name=self.light_color(current_activity), transition=2, )
class ReminderAutomation(AppBase): """Define a feature for recurring or one time reminders.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_TITLE): str, vol.Required(CONF_MESSAGE): str, vol.Required(CONF_REMINDER_DATE): vol_help.valid_date, vol.Required(CONF_REMINDER_TIME): vol_help.valid_time, vol.Optional(CONF_REPEAT): vol.Schema({ vol.Required(CONF_REPEAT_TYPE): vol.In(REPEAT_TYPES), vol.Required(CONF_REPEAT_FREQ): int, }), }, extra=vol.ALLOW_EXTRA, ), CONF_NOTIFICATIONS: vol.Schema( { vol.Required(CONF_TARGETS): vol.In(PERSONS.keys()), vol.Optional(CONF_INTERVAL): int, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.reminder_time = self.properties[CONF_REMINDER_TIME] self.reminder_date = datetime.strptime( self.properties[CONF_REMINDER_DATE], "%d.%m.%Y") self.repeat_type = self.properties[CONF_REPEAT][CONF_REPEAT_TYPE] self.repeat_freq = self.properties[CONF_REPEAT][CONF_REPEAT_FREQ] self.run_daily( self.check_reminder_date, self.parse_time(self.properties[CONF_REMINDER_TIME]), constrain_app_enabled=1, ) self.listen_event( self.disable_on_push_notification, "html5_notification.clicked", action=DONE, constrain_app_enabled=1, ) def check_reminder_date(self, kwargs: dict) -> None: """Check if today is a reminder day, if yes send reminder.""" days_to_reminder_date = (datetime.today() - self.reminder_date).days divisor = None if self.repeat_type == DAYS: divisor = self.repeat_freq elif self.repeat_type == WEEKS: divisor = self.repeat_freq * 7 elif self.repeat_type == MONTHS: divisor = self.repeat_freq * 30 if days_to_reminder_date % divisor == 0: self.send_reminder() self.log("Erinnerungstag! Erinnerung gesendet.") def send_reminder(self) -> None: """Send a repeating actionable push notification as a reminder.""" self.handles[REMINDER] = self.notification_app.notify( kind="repeat", level="home", title=self.properties[CONF_TITLE], message=self.properties[CONF_MESSAGE], targets=self.notifications[CONF_TARGETS], interval=self.notifications[CONF_INTERVAL] * 60, data={"actions": [{ "action": DONE, "title": "Erledigt" }]}, ) def disable_on_push_notification(self, event_name: str, data: dict, kwargs: dict) -> None: """Disable reminder when 'done' got clicked on push notification.""" if REMINDER in self.handles: self.handles.pop(REMINDER)() self.log("Aufgabe erledigt! Schalte Erinnerung aus.")
class VacuumAutomation(AppBase): """Define a feature for scheduled cleaning cycle including cancellation when someone arrives home.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITIES: vol.Schema({vol.Required(VACUUM): vol_help.entity_id}, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({vol.Optional(CONF_CLEANING_TIME): str}, extra=vol.ALLOW_EXTRA), CONF_NOTIFICATIONS: vol.Schema( {vol.Required(CONF_TARGETS): vol.In(PERSONS.keys())}, extra=vol.ALLOW_EXTRA, ), }) class VacuumState(Enum): """Define an enum for vacuum states.""" charging = "Charging" running = "Running" returning = "User Docking" stuck = "Stuck" def configure(self) -> None: """Configure.""" self.started_by_app = False self.vacuum = self.entities[VACUUM] cleaning_time = self.parse_time( self.properties.get(CONF_CLEANING_TIME, "11:00:00")) # scheduled clean cycle self.run_daily(self.start_cleaning, cleaning_time, constrain_app_enabled=1) # cycle finished self.listen_state( self.cleaning_finished, self.vacuum, attribute=STATUS, old=self.VacuumState.returning.value, new=self.VacuumState.charging.value, ) # cancel cycle if someone arrives home self.listen_state(self.cancel_cleaning, HOUSE[PRESENCE_STATE]) # notify when vacuum stuck self.listen_state( self.vacuum_stuck, self.vacuum, attribute=STATUS, new=self.VacuumState.stuck.value, ) # turn on/off cleaning mode when cleaning/finished cleaning self.listen_state(self.set_cleaning_mode_input_boolean, self.vacuum, attribute=STATUS) def start_cleaning(self, kwargs: dict) -> None: """Start the scheduled cleaning cycle.""" self.call_service("vacuum/start_pause", entity_id=self.vacuum) self.started_by_app = True self.log("Pedro startet die Reinigung!") def cleaning_finished(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Deactivate input boolean when cleaning cycle finished.""" self.started_by_app = False self.log("Pedro hat die Reinigung beendet") def cancel_cleaning(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Cancel the cleaning cycle when someone arrives home.""" if (not self.presence_app.noone_home and self.started_by_app ) and self.vacuum_state == self.VacuumState.running.value: self.call_service("vacuum/return_to_base", entity_id=self.vacuum) self.started_by_app = False self.log("Jemand ist gerade angekommen, beende Reiningung!") def vacuum_stuck(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Notify that the vacuum is stuck.""" self.notification_app.notify( kind="single", level="emergency", title="Pedro steckt fest!", message="Pedro steckt fest und braucht Hilfe!", targets=self.notifications["targets"], ) self.log("Pedro steckt fest, sende Benachrichtigung.") def set_cleaning_mode_input_boolean(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Set the input boolean for the cleaning mode.""" if "cleaning_mode" in MODES and old != new: if new == self.VacuumState.running.value: self.turn_on(MODES[CLEANING_MODE]) elif new in (self.VacuumState.charging.value, self.VacuumState.stuck.value): self.turn_off(MODES[CLEANING_MODE]) @property def vacuum_state(self) -> "VacuumState": """Return the current state of the vacuum cleaner.""" return self.get_state(self.vacuum, attribute="status")
class AreaLighting(AppBase): """Define a class for Area Lighting.""" # pylint: disable=too-many-instance-attributes APP_SCHEMA = APP_SCHEMA.extend({ vol.Required("area"): str, vol.Required("motion_sensors"): cv.entity_ids, vol.Optional("delay_off", default=600): vol.All(vol.Coerce(int), vol.Range(min=1, max=3600)), vol.Optional("lights"): cv.entity_ids, vol.Optional("lights_ct"): cv.entity_ids, vol.Optional("lights_rgb"): cv.entity_ids, vol.Optional("default_brightness", default=80): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), vol.Optional("lux_sensor"): cv.entity_id, vol.Optional("lux_threshold", default=100): vol.Coerce(int), vol.Optional("sleep_lights"): cv.entity_ids, vol.Optional("sleep_lights_ct"): cv.entity_ids, vol.Optional("sleep_brightness"): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), vol.Optional("circadian_sensor"): cv.entity_id, vol.Optional("min_brightness", default=1): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), vol.Optional("max_brightness", default=100): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), vol.Optional("min_colortemp", default=2500): vol.All(vol.Coerce(int), vol.Range(min=1000, max=12000)), vol.Optional("max_colortemp", default=5500): vol.All(vol.Coerce(int), vol.Range(min=1000, max=12000)), vol.Optional("transition", default=60): vol.All(vol.Coerce(int), vol.Range(min=1, max=3600)), vol.Optional("update_interval", default=300): vol.All(vol.Coerce(int), vol.Range(min=1, max=3600)), }) def configure(self) -> None: """Configure.""" self.area_id = self.args["area"] self.motion_sensors = self.args["motion_sensors"] self.delay_off = self.args.get("delay_off") self.lights = self.args.get("lights") self.lights_ct = self.args.get("lights_ct") self.lights_rgb = self.args.get("lights_rgb") self.default_brightness = self.args.get("default_brightness") self.lux_sensor = self.args.get("lux_sensor") self.lux_threshold = self.args.get("lux_threshold") self.sleep_lights = self.args.get("sleep_lights") self.sleep_lights_ct = self.args.get("sleep_lights_ct") self.sleep_brightness = self.args.get("sleep_brightness") self.circadian_sensor = self.args.get("circadian_sensor") self.min_brightness = self.args.get("min_brightness") self.max_brightness = self.args.get("max_brightness") self.min_colortemp = self.args.get("min_colortemp") self.max_colortemp = self.args.get("max_colortemp") self.transition = self.args.get("transition") self.update_interval = self.args.get("update_interval") # Build area entity and get friendly name self.area_entity = f"area.{self.area_id}" self.area_name = self.adbase.get_state(self.area_entity, attribute="friendly_name") # Create a list of all lights in the area lights = [ self.lights, self.lights_ct, self.lights_rgb, self.sleep_lights, self.sleep_lights_ct, ] lights = [light for light in lights if light] self.all_lights = set(chain(*lights)) # Listen for motion detected for sensor in self.motion_sensors: self.hass.listen_state(self.on_motion, sensor, new="on") # Listen for changes in light state if self.circadian_sensor: for light in self.all_lights: self.hass.listen_state(self.on_light_change, light) # Listen for occupancy changes of area self.adbase.listen_state(self.on_occupancy_change, self.area_entity, attribute="occupied", new=False) def on_motion(self, entity: str, attribute: str, old: str, new: str, kwargs: dict) -> None: """Respond when motion is detected.""" self.adbase.log(f"Motion detected: {self.area_name}") # Turn lights on if not already on if not self.lights_on(): self.turn_lights_on() # Set motion state of room to True self.set_area_motion(True) # Start/Restart timer to turn motion state to False self.restart_motion_timer() def on_light_change(self, entity: str, attribute: str, old: str, new: str, kwargs: dict) -> None: """Respond when light changes state.""" if new != old: if new == "on": if "circadian_timer" in self.handles: self.adbase.cancel_timer(self.handles["circadian_timer"]) self.handles.pop("circadian_timer") self.handles["circadian_timer"] = self.adbase.run_every( self.turn_lights_on, f"now+{self.update_interval}", self.update_interval, transition=self.transition, ) elif new == "off": # Set motion to False and cancel any existing timers if "motion_timer" in self.handles: self.set_area_motion(False) self.adbase.cancel_timer(self.handles["motion_timer"]) self.handles.pop("motion_timer") if "circadian_timer" in self.handles: self.adbase.cancel_timer(self.handles["circadian_timer"]) self.handles.pop("circadian_timer") def on_occupancy_change(self, entity: str, attribute: str, old: str, new: str, kwargs: dict) -> None: """Respond when occupancy of area changed to False.""" for light in self.all_lights: self.hass.turn_off(light) def turn_lights_on(self, *args: list, **kwargs: dict) -> None: """Turn on lights.""" if not self.lux_above_threshold(): if self.is_sleep() and self.sleep_brightness: lights = self.sleep_lights lights_ct = self.sleep_lights_ct lights_rgb = [] else: lights = self.lights lights_ct = self.lights_ct lights_rgb = self.lights_rgb brightness_pct = int(self.calc_brightness_pct()) colortemp = int(self.calc_colortemp(brightness_pct)) mired = color_temperature_kelvin_to_mired(colortemp) rgb = tuple(map(int, color_temperature_to_rgb(colortemp))) transition = args[0]["transition"] if args else 1 if lights: for light in lights: self.hass.turn_on(light, brightness_pct=brightness_pct, transition=transition) if lights_ct: for light in lights_ct: self.hass.turn_on( light, brightness_pct=brightness_pct, color_temp=mired, transition=transition, ) if lights_rgb: for light in lights_rgb: self.hass.turn_on( light, brightness_pct=brightness_pct, rgb_color=rgb, transition=transition, ) def set_area_motion(self, motion: bool) -> None: """Set motion of area.""" self.adbase.set_state(self.area_entity, motion=motion) def restart_motion_timer(self) -> None: """Set/Reset timer to set occupany of are to False.""" if "motion_timer" in self.handles: self.adbase.cancel_timer(self.handles["motion_timer"]) self.handles.pop("motion_timer") self.handles["motion_timer"] = self.adbase.run_in( self.disable_area_motion, self.delay_off) def disable_area_motion(self, *args: list) -> None: """Set area motion to False.""" self.set_area_motion(False) def calc_brightness_pct(self) -> float: """Calculate brightness percentage.""" if self.is_sleep() and self.sleep_brightness: return self.sleep_brightness if self.circadian_sensor: brightness_pct = self.hass.get_state(self.circadian_sensor) if float(brightness_pct) > 0: return self.max_brightness return ((self.max_brightness - self.min_brightness) * ( (100 + float(brightness_pct)) / 100)) + self.min_brightness return self.default_brightness def calc_colortemp(self, brightness_pct: float) -> float: """Calculate color temperature based on brightness.""" if brightness_pct > 0: return ((self.max_colortemp - self.min_colortemp) * (brightness_pct / 100)) + self.min_colortemp return self.min_colortemp def lux_above_threshold(self) -> bool: """Return true if lux is above threshold.""" if self.lux_sensor: value = float(self.hass.get_state(self.lux_sensor)) return value > self.lux_threshold return False def lights_on(self) -> list: """Return lights currently on.""" return [ entity for entity in self.all_lights if self.hass.get_state(entity) == "on" ] def is_sleep(self) -> bool: """Return true if someone is asleep.""" sleep_state = self.adbase.get_state(self.area_entity, attribute="sleep_state") return sleep_state != "nobody_in_bed"
class NotifyWhenWasherDone(AppBase): """Define a feature to send a notification when the washer has finished.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({vol.Required(vol.In(THRESHOLDS_TYPES)): int}, extra=vol.ALLOW_EXTRA), CONF_NOTIFICATIONS: vol.Schema( { vol.Required(CONF_TARGETS): vol.In(PERSONS.keys()), vol.Required(CONF_MSG): str, vol.Optional(CONF_INTERVAL): int, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.listen_state( self.power_changed, self.manager.entities[CONF_POWER], constrain_app_enabled=1, ) self.listen_state( self.status_changed, self.manager.entities[CONF_STATUS], constrain_app_enabled=1, ) self.listen_event( self.cancel_on_push_notification, "html5_notification.clicked", action=DONE, constrain_app_enabled=1, ) def power_changed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Set washer state based on power change.""" power = float(new) thresholds = self.properties[CONF_THRESHOLDS] if (self.manager.washer_state != self.manager.WasherStates.running and power >= thresholds[CONF_RUNNING_THRESHOLD]): self.manager.washer_state = self.manager.WasherStatus.running self.log("Washer set to Running") elif (self.manager.washer_state == self.manager.WasherStates.running and power <= thresholds[CONF_DRYING_THRESHOLD]): self.manager.washer_state = self.manager.WasherStatus.drying self.log("Washer set to Drying") elif (self.manager.washer_state == self.manager.WasherStates.drying and power == thresholds[CONF_CLEAN_THRESHOLD]): self.manager.washer_state = self.manager.WasherStatus.clean self.log("Washer set to Clean") def status_changed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Take action based on status change.""" if new == self.manager.WasherStates.clean.value: self.handles[WASHER_CLEAN] = self.notification_app.notify( kind="repeat", level="home", title="Waschgerät fertig!", message=self.notifications[CONF_MSG], targets=self.notifications[CONF_TARGETS], interval=self.notifications[CONF_INTERVAL] * 60, data={"actions": [{ "action": DONE, "title": "Erledigt" }]}, ) self.log("Waschgerät fertig! Benachrichtige zum Leeren!") elif old == self.manager.WasherStates.clean.value: if WASHER_CLEAN in self.handles: self.handles.pop(WASHER_CLEAN)() self.log("Waschgerät geleert. Schalte Benachrichtigung aus.") def cancel_on_push_notification(self, event_name: str, data: dict, kwargs: dict) -> None: """Cancel the notification when push notification got clicked.""" self.manager.washer_state = self.manager.WasherStates.dirty self.log("Waschgerät wurde geleert!")
class HueDimmerSwitch(SwitchBase): """Define a Hue Dimmer Switch automation object.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITIES: vol.Schema( {vol.Required(CONF_SWITCH): str}, extra=vol.ALLOW_EXTRA ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_BUTTON_CONFIG): vol.Schema( { vol.Optional(vol.In(BUTTON_PRESSES)): vol.Schema( { vol.Required(CONF_ACTION_TYPE): vol.In( ACTION_TYPES ), vol.Required( CONF_ACTION_ENTITY ): vol_help.entity_id, vol.Optional(CONF_DELAY): int, vol.Optional(CONF_ACTION): str, vol.Optional(CONF_PARAMETERS): dict, }, extra=vol.ALLOW_EXTRA, ) } ) }, extra=vol.ALLOW_EXTRA, ), } ) def configure(self) -> None: """Configure.""" self.switch = self.entities[CONF_SWITCH] self.button_config = self.properties[CONF_BUTTON_CONFIG] self.button_map = { 1002: SHORT_PRESS_ON, 1003: LONG_PRESS_ON, 2002: SHORT_PRESS_UP, 2003: LONG_PRESS_UP, 3002: SHORT_PRESS_DOWN, 3003: LONG_PRESS_DOWN, 4002: SHORT_PRESS_OFF, 4003: LONG_PRESS_OFF, } self.listen_event( self.button_pressed, DECONZ_EVENT, id=self.switch, constrain_app_enabled=1 ) def button_pressed(self, event_name: str, data: dict, kwargs: dict) -> None: """Take action when button is pressed on dimmer switch.""" button_code = data[CONF_EVENT] button_name = self.button_name(button_code) if button_name in self.button_config: button_conf = self.button_config[button_name] action_type = button_conf[CONF_ACTION_TYPE] action_entity = button_conf[CONF_ACTION_ENTITY] action = button_conf.get(CONF_ACTION) delay = button_conf.get(CONF_DELAY) parameters = button_conf.get(CONF_PARAMETERS, {}) if action_type == "dim": brightness = self.get_state(action_entity, attribute="brightness") or 0 new_brightness = brightness - 25 if new_brightness <= 0: self.turn_off(action_entity) else: self.turn_on(action_entity, brightness=new_brightness) elif action_type == "brighten": brightness = self.get_state(action_entity, attribute="brightness") or 0 new_brightness = brightness + 25 if new_brightness > 255: new_brightness = 255 self.turn_on(action_entity, brightness=new_brightness) else: if delay: self.run_in( self.action_on_schedule, delay * 60, action_type=action_type, state=action, action_entity=action_entity, parameters=parameters, ) else: self.action(action_type, action, action_entity, **parameters) def button_name(self, button_code: int) -> Union[str, None]: """Return the button name for the provided code.""" try: return self.button_map[button_code] except KeyError: return None