class PresenceFailsafe(BaseSwitch): """Define a feature to restrict activation when we're not home.""" APP_SCHEMA = APP_SCHEMA.extend({vol.Required(CONF_SWITCH): cv.entity_id}) def configure(self) -> None: """Configure.""" self.listen_state( self._on_switch_activate, self.args[CONF_SWITCH], new="on", constrain_noone="just_arrived,home", ) def _on_switch_activate( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Turn the switch off if no one is home.""" self.log("No one home; not allowing switch to activate") self.toggle(state="off")
class NotifyOnChange(Base): # pylint: disable=too-few-public-methods """Define a feature to notify us the secure status changes.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITY_IDS: vol.Schema( {vol.Required(CONF_STATE): cv.entity_id}, extra=vol.ALLOW_EXTRA ) } ) def configure(self) -> None: """Configure.""" self._send_notification_func = None # type: Optional[Callable] self.listen_state(self._on_security_system_change, self.entity_ids[CONF_STATE]) def _on_security_system_change( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Send a notification when the security state changes.""" def _send_notification() -> None: """Send the notification.""" send_notification( self, ["person:Aaron", "person:Britt"], 'The security status has changed to "{0}"'.format(new), title="Security Change 🔐", ) if self.enabled: _send_notification() else: self._send_notification_func = _send_notification def on_enable(self) -> None: """Send the notification once the automation is enabled (if appropriate).""" if self._send_notification_func: self._send_notification_func() self._send_notification_func = None
class DoubleTapTimerSwitch(BaseZwaveSwitch): """Define a feature to double tap a switch on for a time.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_TIMER_SLIDER): cv.entity_id, vol.Required(CONF_ZWAVE_DEVICE): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_DURATION): int, }, extra=vol.ALLOW_EXTRA) }) def double_up(self, event_name: str, data: dict, kwargs: dict) -> None: """Turn on the target timer slider with a double up tap.""" self.set_value(self.entity_ids[CONF_TIMER_SLIDER], round(self.properties[CONF_DURATION] / 60))
class AaronAccountability(Base): """Define features to keep me accountable on my phone.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( {vol.Required(CONF_AARON_ROUTER_TRACKER): cv.entity_id}, extra=vol.ALLOW_EXTRA, ) }) def configure(self) -> None: """Configure.""" self.listen_state( self._on_disconnect, self.entity_ids[CONF_AARON_ROUTER_TRACKER], new="not_home", constrain_in_blackout=True, constrain_anyone="home", ) @property def router_tracker_state(self) -> str: """Return the state of Aaron's Unifi tracker.""" return self.get_state(self.entity_ids[CONF_AARON_ROUTER_TRACKER]) def _on_disconnect(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send a notification when I disconnect during a blackout.""" self._send_notification() def _send_notification(self) -> None: """Send notification to my love.""" send_notification( self, "ios_brittany_bachs_iphone", "His phone shouldn't be off wifi during the night.", title="Check on Aaron", )
class SimpliSafePIN(PIN): """Define a PIN manager for a SimpliSafe security system.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({vol.Required(CONF_SYSTEM_ID): int}, extra=vol.ALLOW_EXTRA) }) def configure(self) -> None: """Configure.""" super().configure() self._system_id = self.properties[CONF_SYSTEM_ID] def _listen_for_otp_use(self) -> None: """Listen for the use of a one-time PIN.""" self.handles[HANDLE_ONE_TIME_STUB.format( self.name)] = self.listen_state(self._on_otp_used, SIMPLISAFE_ENTITY_ID, attribute="changed_by") def _pin_is_otp(self, pin: str) -> bool: """Return whether a detected PIN is a valid one-time PIN.""" return pin == self.label def remove_pin(self) -> None: """Remove the PIN from SimpliSafe.""" self.call_service("simplisafe/remove_pin", system_id=self._system_id, label_or_pin=self.label) def set_pin(self) -> None: """Add the pin to SimpliSafe.""" self.call_service( "simplisafe/set_pin", system_id=self._system_id, label=self.label, pin=self.value, )
class MonitorConsumables(Base): """Define a feature to notify when a consumable gets low.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_CONSUMABLE_THRESHOLD): int, vol.Required(CONF_CONSUMABLES): cv.ensure_list, }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.triggered = False for consumable in self.properties['consumables']: self.listen_state( self.consumable_changed, self.app.entity_ids['vacuum'], attribute=consumable, constrain_input_boolean=self.enabled_entity_id) def consumable_changed( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Create a task when a consumable is getting low.""" if int(new) < self.properties['consumable_threshold']: if self.triggered: return self.log('Consumable is low: {0}'.format(attribute)) self.notification_manager.create_omnifocus_task( 'Order a new Wolfie consumable: {0}'.format(attribute)) self.triggered = True elif self.triggered: self.triggered = False
class DoubleTapToggleSwitch(BaseZwaveSwitch): """Define a feature to toggle a switch with a double tab of this switch.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_TARGET): cv.entity_id, vol.Required(CONF_ZWAVE_DEVICE): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_DURATION): int, }, extra=vol.ALLOW_EXTRA) }) def double_down(self, event_name: str, data: dict, kwargs: dict) -> None: """Turn off the target switch with a double down tap.""" self.turn_off(self.entity_ids[CONF_TARGET]) def double_up(self, event_name: str, data: dict, kwargs: dict) -> None: """Turn on the target switch with a double up tap.""" self.turn_on(self.entity_ids[CONF_TARGET])
class Person(Base): """Define a class to represent a person.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_DEVICE_TRACKERS): cv.ensure_list, vol.Required(CONF_NOTIFIERS): cv.ensure_list, vol.Required(CONF_PRESENCE_STATUS_SENSOR): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Optional(CONF_PUSH_DEVICE_ID): str, }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" # Get the raw state of the device trackers and seed the home state: self._raw_state = self._most_common_raw_state() if self._raw_state == 'home': self._home_state = self.presence_manager.HomeStates.home else: self._home_state = self.presence_manager.HomeStates.away # Store a global reference to this person: self.global_vars.setdefault(CONF_PEOPLE, []) self.global_vars[CONF_PEOPLE].append(self) # Listen for changes to any of the person's device trackers: for device_tracker in self.entity_ids[CONF_DEVICE_TRACKERS]: kind = self.get_state(device_tracker, attribute='source_type') if kind == 'router': self.listen_state(self._device_tracker_changed_cb, device_tracker, old='not_home') else: self.listen_state(self._device_tracker_changed_cb, device_tracker) # Render the initial state of the presence sensor: self._render_presence_status_sensor() @property def first_name(self) -> str: """Return the person's name.""" return self.name.title() @property def home_state(self) -> Enum: """Return the person's human-friendly home state.""" return self._home_state @home_state.setter def home_state(self, state: Enum) -> None: """Set the home-friendly home state.""" original_state = self._home_state self._home_state = state self._fire_presence_change_event(original_state, state) @property def notifiers(self) -> list: """Return the notifiers associated with the person.""" return self.entity_ids[CONF_NOTIFIERS] @property def push_device_id(self) -> str: """Get the iOS device ID for push notifications.""" return self.properties.get(CONF_PUSH_DEVICE_ID) def _check_transition_cb(self, kwargs: dict) -> None: """Transition the user's home state (if appropriate).""" current_state = kwargs['current_state'] if not self._home_state == kwargs['current_state']: return if current_state == self.presence_manager.HomeStates.just_arrived: self.home_state = self.presence_manager.HomeStates.home elif current_state == self.presence_manager.HomeStates.just_left: self.home_state = self.presence_manager.HomeStates.away elif current_state == self.presence_manager.HomeStates.away: self.home_state = self.presence_manager.HomeStates.extended_away # Re-render the sensor: self._render_presence_status_sensor() def _device_tracker_changed_cb(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Respond when a device tracker changes.""" if self._raw_state == new: return self._raw_state = new # Cancel any old timers: for handle in (HANDLE_5_MINUTE_TIMER, HANDLE_24_HOUR_TIMER): if handle in self.handles: handle = self.handles.pop(handle) self.cancel_timer(handle) # Set the home state and schedule transition checks (Just Left -> Away, # for example) for various points in the future: if new == 'home': self.home_state = self.presence_manager.HomeStates.just_arrived self.handles[HANDLE_5_MINUTE_TIMER] = self.run_in( self._check_transition_cb, 60 * 5, current_state=self.presence_manager.HomeStates.just_arrived) elif old == 'home': self.home_state = self.presence_manager.HomeStates.just_left self.handles[HANDLE_5_MINUTE_TIMER] = self.run_in( self._check_transition_cb, 60 * 5, current_state=self.presence_manager.HomeStates.just_left) self.handles[HANDLE_24_HOUR_TIMER] = self.run_in( self._check_transition_cb, 60 * 60 * 24, current_state=self.presence_manager.HomeStates.away) # Re-render the sensor: self._render_presence_status_sensor() def _fire_presence_change_event(self, old: Enum, new: Enum) -> None: """Fire a presence change event.""" if new in (self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home): states = [ self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home ] else: states = [new] first = self.presence_manager.only_one(*states) self.fire_event('PRESENCE_CHANGE', person=self.first_name, old=old.value, new=new.value, first=first) def _most_common_raw_state(self) -> str: """Get the most common raw state from the person's device trackers.""" return most_common([ self.get_tracker_state(dt) for dt in self.entity_ids[CONF_DEVICE_TRACKERS] ]) def _render_presence_status_sensor(self) -> None: """Update the sensor in the UI.""" if self._home_state in (self.presence_manager.HomeStates.home, self.presence_manager.HomeStates.just_arrived): picture_state = 'home' else: picture_state = 'away' if self._home_state: state = self._home_state.value else: state = self._raw_state self.set_state(self.entity_ids[CONF_PRESENCE_STATUS_SENSOR], state=state, attributes={ 'friendly_name': self.first_name, 'entity_picture': '/local/{0}-{1}.png'.format(self.name, picture_state), })
SERVICE_ORDER_SEQUENTIAL = "sequential" SERVICE_ORDER_OPTIONS = set([SERVICE_ORDER_RANDOM, SERVICE_ORDER_SEQUENTIAL]) DEFAULT_RANDOM_TICK_LOWER_END = 5 * 60 DEFAULT_RANDOM_TICK_UPPER_END = 60 * 60 HANDLE_TICK = "tick" SERVICE_CALL_SCHEMA = vol.Schema({ vol.Required(CONF_SERVICE): cv.string, vol.Optional(CONF_SERVICE_DATA, default={}): dict, }) SINGLE_SERVICE_SCHEMA = APP_SCHEMA.extend( {vol.Required(CONF_SERVICES): SERVICE_CALL_SCHEMA}) MULTI_SERVICE_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_SERVICES): vol.All(cv.ensure_list, [SERVICE_CALL_SCHEMA]), vol.Optional(CONF_SERVICE_ORDER, default=SERVICE_ORDER_SEQUENTIAL): vol.In(SERVICE_ORDER_OPTIONS), }) class MultiServiceBase(Base): """Define a base class for automations that handle multiple services.""" def configure(self) -> None: """Configure.""" self._count = 0
class ScheduledCycle(Base): """Define a feature to run the vacuum on a schedule.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_IOS_EMPTIED_KEY): str, vol.Required(CONF_SCHEDULE_SWITCHES): cv.ensure_list, vol.Required(CONF_SCHEDULE_TIME): str, }, extra=vol.ALLOW_EXTRA, ) }) @property def active_days(self) -> list: """Get the days that the vacuuming schedule should run.""" on_days = [] for toggle in self.properties["schedule_switches"]: state = self.get_state(toggle, attribute="all") if state["state"] == "on": on_days.append(state["attributes"]["friendly_name"]) return on_days def configure(self) -> None: """Configure.""" self.initiated_by_app = False self._create_schedule() self.listen_event(self._on_security_system_change, EVENT_ALARM_CHANGE, constrain_enabled=True) self.listen_event(self._on_switch_start, EVENT_VACUUM_START, constrain_enabled=True) self.listen_state( self._on_vacuum_cycle_done, self.app.entity_ids[CONF_STATUS], old=self.app.States.returning.value, new=self.app.States.docked.value, constrain_enabled=True, ) for toggle in self.properties[CONF_SCHEDULE_SWITCHES]: self.listen_state(self._on_schedule_change, toggle, constrain_enabled=True) def _create_schedule(self) -> None: """Create the vacuuming schedule from the on booleans.""" if HANDLE_SCHEDULE in self.handles: cancel = self.handles.pop(HANDLE_SCHEDULE) cancel() self.handles[HANDLE_SCHEDULE] = run_on_days( self, self._on_schedule_start, self.active_days, self.parse_time(self.properties["schedule_time"]), constrain_enabled=True, ) def _on_schedule_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Reload the schedule when one of the input booleans change.""" self._create_schedule() def _on_security_system_change(self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to 'ALARM_CHANGE' events.""" state = self.app.States( self.get_state(self.app.entity_ids[CONF_STATUS])) # Scenario 1: Vacuum is charging and is told to start: if ( self.initiated_by_app and state == self.app.States.docked ) and data["state"] == self.security_manager.AlarmStates.home.value: self.log("Activating vacuum (post-security)") self.turn_on(self.app.entity_ids[CONF_VACUUM]) # Scenario 2: Vacuum is running when alarm is set to "Away": elif (state == self.app.States.cleaning and data["state"] == self.security_manager.AlarmStates.away.value): self.log('Security mode is "Away"; pausing until "Home"') self.call_service("vacuum/start_pause", entity_id=self.app.entity_ids[CONF_VACUUM]) self.security_manager.set_alarm( self.security_manager.AlarmStates.home) # Scenario 3: Vacuum is paused when alarm is set to "Home": elif (state == self.app.States.paused and data["state"] == self.security_manager.AlarmStates.home.value): self.log('Alarm in "Home"; resuming') self.call_service("vacuum/start_pause", entity_id=self.app.entity_ids[CONF_VACUUM]) def _on_vacuum_cycle_done(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Re-arm security (if needed) when done.""" self.log("Vacuuming cycle all done") if self.presence_manager.noone( self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home, ): self.log('Changing alarm state to "away"') self.security_manager.set_alarm( self.security_manager.AlarmStates.away) self.app.bin_state = self.app.BinStates.full self.initiated_by_app = False def _on_schedule_start(self, kwargs: dict) -> None: """Start cleaning via the schedule.""" if not self.initiated_by_app: self.app.start() self.initiated_by_app = True def _on_switch_start(self, event_name: str, data: dict, kwargs: dict) -> None: """Start cleaning via the switch.""" if not self.initiated_by_app: self.app.start() self.initiated_by_app = True
class Vacuum(Base): """Define an app to represent a vacuum-type appliance.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_BIN_STATE): cv.entity_id, vol.Required(CONF_VACUUM): cv.entity_id, }) class BinStates(Enum): """Define an enum for vacuum bin states.""" empty = "Empty" full = "Full" class States(Enum): """Define an enum for vacuum states.""" cleaning = "cleaning" docked = "docked" error = "error" idle = "idle" paused = "paused" returning = "returning" unavailable = "unavailable" def configure(self) -> None: """Configure.""" self.listen_state( self._on_cycle_done, self.args[CONF_VACUUM], old=self.States.returning.value, new=self.States.docked.value, ) self.listen_state(self._on_schedule_start, self.args[CONF_CALENDAR], new="on") @property def bin_state(self) -> "BinStates": """Define a property to get the bin state.""" return self.BinStates(self.get_state(self.args[CONF_BIN_STATE])) @bin_state.setter def bin_state(self, value: "BinStates") -> None: """Set the bin state.""" self.select_option(self.args[CONF_BIN_STATE], value.value) @property def run_time(self) -> int: """Return the most recent amount of running time.""" return int(self.get_state(self.args[CONF_RUN_TIME])) @property def state(self) -> "States": """Define a property to get the state.""" return self.States(self.get_state(self.args[CONF_VACUUM])) def _on_cycle_done(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Re-arm security (if needed) when done.""" self.log("Vacuuming cycle all done") if self.presence_manager.noone( self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home, ): self.log('Changing alarm state to "away"') self.security_manager.set_alarm( self.security_manager.AlarmStates.away) if self.run_time >= self.args[CONF_FULL_THRESHOLD_MINUTES]: self.bin_state = self.BinStates.full def _on_schedule_start(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Start cleaning via the schedule.""" self.start() def pause(self) -> None: """Pause the cleaning cycle.""" self.call_service("vacuum/pause", entity_id=self.args[CONF_VACUUM]) def start(self) -> None: """Start a cleaning cycle.""" self.log("Starting vacuuming cycle") if self.security_manager.alarm_state == self.security_manager.AlarmStates.away: self.log('Changing alarm state to "Home"') self.security_manager.set_alarm( self.security_manager.AlarmStates.home) else: self.log("Activating vacuum") self.call_service("vacuum/start", entity_id=self.args[CONF_VACUUM]) def stop(self) -> None: """Stop a vacuuming cycle.""" self.log("Stopping vacuuming cycle") self.call_service("vacuum/return_to_base", entity_id=self.args[CONF_VACUUM])
class Person(Base): """Define a class to represent a person.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_PERSON): cv.entity_id, vol.Required(CONF_NOTIFIERS): cv.ensure_list, vol.Required(CONF_PRESENCE_STATUS_SENSOR): cv.entity_id, }, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( {vol.Optional(CONF_PUSH_DEVICE_ID): str}, extra=vol.ALLOW_EXTRA ), } ) def configure(self) -> None: """Configure.""" # "Seed" the person's non-binary state: self._last_raw_state = self.state if self.state == "home": self._non_binary_state = self.presence_manager.HomeStates.home else: self._non_binary_state = self.presence_manager.HomeStates.away # Store a global reference to this person: if CONF_PEOPLE not in self.global_vars: self.global_vars[CONF_PEOPLE] = [] self.global_vars[CONF_PEOPLE].append(self) # Listen to state changes for the `person` entity: self.listen_state(self._on_person_state_change, self.entity_ids[CONF_PERSON]) # # Render the initial state of the presence sensor: self._render_presence_status_sensor() @property def first_name(self) -> str: """Return the person's name.""" return self.name.title() @property def non_binary_state(self) -> "PresenceManager.HomeStates": """Return the person's human-friendly non-binary state.""" return self._non_binary_state @non_binary_state.setter def non_binary_state(self, state: "PresenceManager.HomeStates") -> None: """Set the home-friendly home state.""" original_state = self._non_binary_state self._non_binary_state = state self._fire_presence_change_event(original_state, state) @property def notifiers(self) -> list: """Return the notifiers associated with the person.""" return self.entity_ids[CONF_NOTIFIERS] @property def state(self) -> str: """Get the person's raw entity state.""" return self.get_state(self.entity_ids[CONF_PERSON]) def _fire_presence_change_event( self, old: "PresenceManager.HomeStates", new: "PresenceManager.HomeStates" ) -> None: """Fire a presence change event.""" if new in ( self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home, ): states = [ self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home, ] else: states = [new] first = self.presence_manager.only_one(*states) self.fire_event( EVENT_PRESENCE_CHANGE, person=self.first_name, old=old.value, new=new.value, first=first, ) def _on_person_state_change( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Respond when the person entity changes state.""" # `person` entities can update their state to the same value as before; if this # occurs, return immediately: if self._last_raw_state == new: return self._last_raw_state = new # Cancel any old timers: for handle_key in (HANDLE_5_MINUTE_TIMER, HANDLE_24_HOUR_TIMER): if handle_key not in self.handles: continue handle = self.handles.pop(handle_key) self.cancel_timer(handle) # Set the home state and schedule transition checks (Just Left -> Away, # for example) for various points in the future: if new == "home": self.non_binary_state = self.presence_manager.HomeStates.just_arrived self.handles[HANDLE_5_MINUTE_TIMER] = self.run_in( self._on_transition_state, TRANSITION_DURATION_JUST_ARRIVED, current_state=self.presence_manager.HomeStates.just_arrived, ) elif old == "home": self.non_binary_state = self.presence_manager.HomeStates.just_left self.handles[HANDLE_5_MINUTE_TIMER] = self.run_in( self._on_transition_state, TRANSITION_DURATION_JUST_LEFT, current_state=self.presence_manager.HomeStates.just_left, ) self.handles[HANDLE_24_HOUR_TIMER] = self.run_in( self._on_transition_state, TRANSITION_DURATION_AWAY, current_state=self.presence_manager.HomeStates.away, ) # Re-render the sensor: self._render_presence_status_sensor() def _on_transition_state(self, kwargs: dict) -> None: """Transition the user's home state (if appropriate).""" current_state = kwargs["current_state"] if current_state == self.presence_manager.HomeStates.just_arrived: self.non_binary_state = self.presence_manager.HomeStates.home elif current_state == self.presence_manager.HomeStates.just_left: self.non_binary_state = self.presence_manager.HomeStates.away elif current_state == self.presence_manager.HomeStates.away: self.non_binary_state = self.presence_manager.HomeStates.extended_away # Re-render the sensor: self._render_presence_status_sensor() def _render_presence_status_sensor(self) -> None: """Update the sensor in the UI.""" if self._last_raw_state == "home": picture_state = "home" else: picture_state = "away" if self.state in ("home", "not_home"): state = self._non_binary_state.value else: state = self.state self.set_state( self.entity_ids[CONF_PRESENCE_STATUS_SENSOR], state=state, attributes={ "friendly_name": self.first_name, "entity_picture": "/local/{0}-{1}.png".format(self.name, picture_state), }, )
ENTITY_IDS_SCHEMA = vol.Schema( { vol.Required(CONF_AVAILABLE): cv.entity_id, vol.Required(CONF_INSTALLED): cv.entity_id, }, extra=vol.ALLOW_EXTRA) PROPERTIES_SCHEMA = vol.Schema({ vol.Required(CONF_APP_NAME): str, }, extra=vol.ALLOW_EXTRA) VERSION_APP_SCHEMA = APP_SCHEMA.extend({ vol.Optional(CONF_ENTITY_IDS): ENTITY_IDS_SCHEMA, vol.Optional(CONF_PROPERTIES): PROPERTIES_SCHEMA, }) DYNAMIC_APP_SCHEMA = VERSION_APP_SCHEMA.extend({ vol.Required(CONF_PROPERTIES): PROPERTIES_SCHEMA.extend({ vol.Required(CONF_CREATED_ENTITY_ID): cv.entity_id, vol.Required(CONF_FRIENDLY_NAME): str, vol.Required(CONF_ICON): str, vol.Required(CONF_UPDATE_INTERVAL): int, }) }) class NewVersionNotification(Base):
class AbsentInsecure(Base): """Define a feature to notify us when we've left home insecure.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_STATE): cv.entity_id, }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.listen_event( self.response_from_push_notification, 'ios.notification_action_fired', actionName='LOCK_UP_AWAY', constrain_input_boolean=self.enabled_entity_id, action='away') self.listen_event( self.response_from_push_notification, 'ios.notification_action_fired', actionName='LOCK_UP_HOME', constrain_input_boolean=self.enabled_entity_id, action='home') self.listen_state( self.house_insecure, self.entity_ids[CONF_STATE], new='Open', duration=60 * 5, constrain_input_boolean=self.enabled_entity_id, constrain_noone='just_arrived,home') def house_insecure( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notifications when the house has been left insecure.""" self.log('No one home and house is insecure; notifying') self.notification_manager.send( "No one is home and the house isn't locked up.", title='Security Issue 🔐', blackout_start_time=None, blackout_end_time=None, target=['everyone', 'slack'], data={'push': { 'category': 'security' }}) def response_from_push_notification( self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to 'ios.notification_action_fired' events.""" target = self.notification_manager.get_target_from_push_id( data['sourceDevicePermanentID']) if kwargs['action'] == 'home': self.turn_on('scene.good_night') elif kwargs['action'] == 'away': self.turn_on('scene.depart_home') self.notification_manager.send( '{0} locked up the house.'.format(target.first_name), title='Issue Resolved 🔐', target=['not {0}'.format(target.first_name), 'slack'])
class GarageLeftOpen(Base): # pylint: disable=too-few-public-methods """Define a feature to notify us when the garage is left open.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_GARAGE_DOOR): cv.entity_id, vol.Required(CONF_NOTIFICATION_INTERVAL): vol.All(cv.time_period, lambda value: value.seconds), vol.Required(CONF_TIME_LEFT_OPEN): vol.All(cv.time_period, lambda value: value.seconds), }) def configure(self) -> None: """Configure.""" self.listen_state(self._on_closed, self.args[CONF_GARAGE_DOOR], new="closed") self.listen_state( self._on_left_open, self.args[CONF_GARAGE_DOOR], new="open", duration=self.args[CONF_TIME_LEFT_OPEN], ) def _cancel_notification_cycle(self) -> None: """Cancel any active notification.""" if HANDLE_GARAGE_OPEN in self.data: cancel = self.data.pop(HANDLE_GARAGE_OPEN) cancel() def _on_closed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Cancel notification when the garage is _on_closed.""" self._cancel_notification_cycle() def _on_left_open(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notifications when the garage has been left open.""" if self.enabled: self._start_notification_cycle() else: self._cancel_notification_cycle() def _start_notification_cycle(self) -> None: """Start the notification cycle.""" message = "The garage has been left open. Want to close it?" self.data[HANDLE_GARAGE_OPEN] = send_notification( self, ["person:Aaron", "person:Britt"], message, title="Garage Open 🚗", when=self.datetime(), interval=self.args[CONF_NOTIFICATION_INTERVAL], data={"push": { "category": "garage" }}, ) self.slack_app_home_assistant.ask( message, { "Yes": { "callback": self.security_manager.close_garage, "response_text": "You got it; closing it now.", }, "No": { "response_text": "If you really say so..." }, }, urgent=True, ) def on_disable(self) -> None: """Stop the notification once the automation is disable.""" self._cancel_notification_cycle() def on_enable(self) -> None: """Send the notification once the automation is enabled.""" if self.get_state(self.args[CONF_GARAGE_DOOR]) == "open": self._start_notification_cycle()
class PiHoleSwitch(Base): # pylint: disable=too-few-public-methods """Define a switch to turn on/off all Pi-hole instances.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( {vol.Required(CONF_PI_HOLE_ACTIVE_SENSOR): cv.entity_id}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_PI_HOLE_API_KEY): str, vol.Required(CONF_PI_HOLE_HOSTS): list, vol.Required(CONF_PI_HOLE_OFF_EVENT): str, vol.Required(CONF_PI_HOLE_ON_EVENT): str, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" if self._is_enabled: self._set_dummy_sensor("on") else: self._set_dummy_sensor("off") self.listen_event(self._on_switch_off, self.properties[CONF_PI_HOLE_OFF_EVENT]) self.listen_event(self._on_switch_on, self.properties[CONF_PI_HOLE_ON_EVENT]) @property def _is_enabled(self) -> bool: """Return whether any Pi-hole hosts are enabled.""" for host in self.properties[CONF_PI_HOLE_HOSTS]: resp = self._request(host, "status") status = resp.json()["status"] if status == "enabled": return True return False def _on_switch_off(self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to the switch being turned off.""" self.disable_pi_hole() def _on_switch_on(self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to the switch being turned on.""" self.enable_pi_hole() def _request(self, host: str, endpoint: str) -> requests.Response: """Send an HTTP request to Pi-hole.""" return requests.get( "http://{0}/admin/api.php?{1}".format(host, endpoint), params={"auth": self.properties[CONF_PI_HOLE_API_KEY]}, ) def _set_dummy_sensor(self, state: str) -> None: """Set the state of off a dummy sensor which informs the switch's state.""" self.set_state( self.entity_ids[CONF_PI_HOLE_ACTIVE_SENSOR], state=state, attributes={"friendly_name": "Pi-hole"}, ) def disable_pi_hole(self) -> None: """Disable Pi-hole.""" self._set_dummy_sensor("off") for host in self.properties[CONF_PI_HOLE_HOSTS]: self._request(host, "disable") def enable_pi_hole(self) -> None: """Enable Pi-hole.""" self._set_dummy_sensor("on") for host in self.properties[CONF_PI_HOLE_HOSTS]: self._request(host, "enable")
class ClimateManager(Base): # pylint: disable=too-many-public-methods """Define an app to represent climate control.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_AWAY_MODE): cv.entity_id, vol.Required(CONF_ECO_HIGH_THRESHOLD): cv.entity_id, vol.Required(CONF_ECO_LOW_THRESHOLD): cv.entity_id, vol.Required(CONF_HUMIDITY_SENSOR): cv.entity_id, vol.Required(CONF_INDOOR_TEMPERATURE_SENSOR): cv.entity_id, vol.Required(CONF_OUTDOOR_BRIGHTNESS_PERCENT_SENSOR): cv.entity_id, vol.Required(CONF_OUTDOOR_BRIGHTNESS_SENSOR): cv.entity_id, vol.Required(CONF_OUTDOOR_HIGH_THRESHOLD): cv.entity_id, vol.Required(CONF_OUTDOOR_LOW_THRESHOLD): cv.entity_id, vol.Required(CONF_OUTDOOR_TEMPERATURE_SENSOR): cv.entity_id, vol.Required(CONF_THERMOSTAT): cv.entity_id, }) }) def configure(self) -> None: """Configure.""" self._last_hvac_mode = None # type: Optional[str] self._last_temperature = None # type: Optional[float] if self.away_mode: self._set_away() self.listen_state(self._on_away_mode_change, self.entity_ids[CONF_AWAY_MODE]) @property def away_mode(self) -> bool: """Return the state of away mode.""" return self.get_state(self.entity_ids[CONF_AWAY_MODE]) == "on" @property def eco_high_temperature(self) -> float: """Return the upper limit of eco mode.""" return float(self.get_state(self.entity_ids[CONF_ECO_HIGH_THRESHOLD])) @eco_high_temperature.setter def eco_high_temperature(self, value: int) -> None: """Set the upper limit of eco mode.""" self.set_value(self.entity_ids[CONF_ECO_HIGH_THRESHOLD], value) @property def eco_low_temperature(self) -> float: """Return the lower limit of eco mode.""" return float(self.get_state(self.entity_ids[CONF_ECO_LOW_THRESHOLD])) @eco_low_temperature.setter def eco_low_temperature(self, value: int) -> None: """Set the upper limit of eco mode.""" self.set_value(self.entity_ids[CONF_ECO_LOW_THRESHOLD], value) @property def fan_mode(self) -> str: """Return the current fan mode.""" return self.get_state(self.entity_ids[CONF_THERMOSTAT], attribute="fan_mode") @property def indoor_humidity(self) -> float: """Return the average indoor humidity.""" return float(self.get_state(self.entity_ids[CONF_HUMIDITY_SENSOR])) @property def indoor_temperature(self) -> float: """Return the average indoor temperature.""" return float( self.get_state(self.entity_ids[CONF_INDOOR_TEMPERATURE_SENSOR])) @property def hvac_mode(self) -> str: """Return the current operating mode.""" return self.get_state(self.entity_ids[CONF_THERMOSTAT]) @property def outdoor_brightness(self) -> float: """Return the outdoor brightness in lux.""" return float(self.get_state(self.entity_ids[CONF_BRIGHTNESS_SENSOR])) @property def outdoor_brightness_percentage(self) -> float: """Return the human-perception of brightness percentage.""" return float( self.get_state(self.entity_ids[CONF_BRIGHTNESS_PERCENT_SENSOR])) @property def outdoor_high_temperature(self) -> float: """Return the upper limit of "extreme" outdoor temperatures.""" return float( self.get_state(self.entity_ids[CONF_OUTDOOR_HIGH_THRESHOLD])) @property def outdoor_low_temperature(self) -> float: """Return the lower limit of "extreme" outdoor temperatures.""" return float( self.get_state(self.entity_ids[CONF_OUTDOOR_LOW_THRESHOLD])) @property def outdoor_temperature(self) -> float: """Return the outdoor temperature.""" return float( self.get_state(self.entity_ids[CONF_OUTDOOR_TEMPERATURE_SENSOR])) @property def outdoor_temperature_extreme(self) -> float: """Return whether the outside temperature is at extreme limits.""" return (self.outdoor_temperature < self.outdoor_low_temperature or self.outdoor_temperature > self.outdoor_high_temperature) @property def target_temperature(self) -> float: """Return the temperature the thermostat is currently set to.""" try: return float( self.get_state(self.entity_ids[CONF_THERMOSTAT], attribute="temperature")) except TypeError: return 0.0 def _on_away_mode_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """React when away mode is toggled.""" if new == "on": self._set_away() else: self._set_home() def _on_eco_temp_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """React when the temperature goes above or below its eco thresholds.""" current_temperature = float(new) if (current_temperature > self.eco_high_temperature and self.hvac_mode != HVAC_MODE_COOL): self.log('Eco Mode: setting to "Cool" ({0}°)'.format( self.eco_high_temperature)) self.set_mode_cool() self.set_temperature(self.eco_high_temperature) elif (current_temperature < self.eco_low_temperature and self.hvac_mode != HVAC_MODE_HEAT): self.log('Eco Mode: setting to "Heat" ({0}°)'.format( self.eco_low_temperature)) self.set_mode_heat() self.set_temperature(self.eco_low_temperature) elif (self.eco_low_temperature <= current_temperature <= self.eco_high_temperature and self.hvac_mode != HVAC_MODE_OFF): self.log('Within eco mode limits; turning thermostat to "Off"') self.set_mode_off() def _set_away(self) -> None: """Put the thermostat in "Away" mode.""" self.log('Setting thermostat to "Away" mode') self.set_mode_off() self.handles[HANDLE_ECO_MODE] = self.listen_state( self._on_eco_temp_change, self.entity_ids[CONF_INDOOR_TEMPERATURE_SENSOR]) def _set_fan_mode(self, fan_mode: str) -> None: """Set the themostat's fan mode.""" if fan_mode == self.fan_mode: return self.log('Setting fan mode to "{0}"'.format(fan_mode.title())) self.call_service( "climate/set_fan_mode", entity_id=self.entity_ids[CONF_THERMOSTAT], fan_mode=fan_mode, ) def _set_home(self) -> None: """Put the thermostat in "Home" mode.""" self.log('Setting thermostat to "Home" mode') handle = self.handles.pop(HANDLE_ECO_MODE) self.cancel_listen_state(handle) # If the thermostat isn't doing anything, set it to the previous settings # (before away mode); otherwise, let it keep doing its thing: if self.hvac_mode == HVAC_MODE_OFF: self._restore_previous_state() def _restore_previous_state(self) -> None: """Restore the thermostat to its previous state.""" if self._last_hvac_mode and self._last_temperature: self._set_hvac_mode(self._last_hvac_mode) self.set_temperature(self._last_temperature) def _set_hvac_mode(self, hvac_mode: str) -> None: """Set the themostat's operation mode.""" if hvac_mode == self.hvac_mode: return self._last_hvac_mode = self.hvac_mode self.log('Setting operation mode to "{0}"'.format(hvac_mode.title())) self.call_service( "climate/set_hvac_mode", entity_id=self.entity_ids[CONF_THERMOSTAT], hvac_mode=hvac_mode, ) def bump_temperature(self, value: int) -> None: """Bump the current temperature.""" if HVAC_MODE_COOL in (self.hvac_mode, self._last_hvac_mode): value *= -1 self.set_temperature(self.target_temperature + value) def set_away(self) -> None: """Set the thermostat to away.""" self.turn_on(self.entity_ids[CONF_AWAY_MODE]) def set_fan_auto_low(self) -> None: """Set the fan mode to auto_low.""" self._set_fan_mode(FAN_MODE_AUTO_LOW) def set_fan_circulate(self) -> None: """Set the fan mode to circulate.""" self._set_fan_mode(FAN_MODE_CIRCULATE) def set_fan_on_low(self) -> None: """Set the fan mode to on_low.""" self._set_fan_mode(FAN_MODE_ON_LOW) def set_home(self) -> None: """Set the thermostat to home.""" self.turn_off(self.entity_ids[CONF_AWAY_MODE]) def set_mode_auto(self) -> None: """Set the operation mode to auto.""" self._set_hvac_mode(HVAC_MODE_AUTO) def set_mode_cool(self) -> None: """Set the operation mode to cool.""" self._set_hvac_mode(HVAC_MODE_COOL) def set_mode_heat(self) -> None: """Set the operation mode to heat.""" self._set_hvac_mode(HVAC_MODE_HEAT) def set_mode_off(self) -> None: """Set the operation mode to off.""" self._last_temperature = self.target_temperature self._set_hvac_mode(HVAC_MODE_OFF) def set_temperature(self, temperature: float) -> None: """Set the thermostat temperature.""" if temperature == self.target_temperature: return self._last_temperature = self.target_temperature # If the thermostat is off and the temperature is adjusted, # make a guess as to which operation mode should be used: if self.hvac_mode == HVAC_MODE_OFF: if temperature > self.indoor_temperature: self.set_mode_heat() elif temperature < self.indoor_temperature: self.set_mode_cool() else: self.set_mode_auto() self.call_service( "climate/set_temperature", entity_id=self.entity_ids[CONF_THERMOSTAT], temperature=str(int(temperature)), ) def toggle(self) -> None: """Toggle the thermostat between off and its previous HVAC state/temp.""" if self.hvac_mode == HVAC_MODE_OFF: self._restore_previous_state() else: self.set_mode_off()
class NotifyBadAqi(Base): """Define a feature to notify us of bad air quality.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({vol.Required(CONF_AQI_SENSOR): cv.entity_id}, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({vol.Required(CONF_AQI_THRESHOLD): int}, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self._bad_notification_sent = False self._good_notification_sent = True self._send_notification_func = None # type: Optional[Callable] self.listen_state(self._on_aqi_change, self.entity_ids[CONF_AQI_SENSOR]) def _on_aqi_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send select notifications when cooling and poor AQI.""" if self.climate_manager.hvac_mode != HVAC_MODE_COOL: return current_aqi = int(new) def _send_bad_notification(): """Send a notification of bad AQI.""" send_notification( self, "presence:home", "AQI is at {0}; consider closing the humidifier vent.".format( current_aqi), title="Poor AQI 😤", ) def _send_good_notification(): """Send a notification of good AQI.""" send_notification( self, "presence:home", "AQI is at {0}; open the humidifer vent again.".format( current_aqi), title="Better AQI 😅", ) if current_aqi > self.properties[CONF_AQI_THRESHOLD]: if self._bad_notification_sent: return self.log("Notifying anyone at home of bad AQI during cooling") self._bad_notification_sent = True self._good_notification_sent = False notification_func = _send_bad_notification else: if self._good_notification_sent: return self.log( "Notifying anyone at home of AQI improvement during cooling") self._bad_notification_sent = False self._good_notification_sent = True notification_func = _send_good_notification # If the automation is enabled when a battery is low, send a notification; # if not, remember that we should send the notification when the automation # becomes enabled: if self.enabled: notification_func() else: self._send_notification_func = notification_func def on_enable(self) -> None: """Send the notification once the automation is enabled (if appropriate).""" if self._send_notification_func: self._send_notification_func() self._send_notification_func = None
class LowBatteries(Base): # pylint: disable=too-few-public-methods """Define a feature to notify us of low batteries.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( {vol.Required(CONF_BATTERIES_TO_MONITOR): cv.ensure_list}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_BATTERY_LEVEL_THRESHOLD): int, vol.Required(CONF_NOTIFICATION_INTERVAL): int, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self._registered = [] # type: List[str] self._send_notification_func = None # type: Optional[Callable] for entity in self.entity_ids[CONF_BATTERIES_TO_MONITOR]: if entity.split(".")[0] == "binary_sensor": self.listen_state(self._on_battery_change, entity, new="on", attribute="all") else: self.listen_state(self._on_battery_change, entity, attribute="all") def _on_battery_change( self, entity: Union[str, dict], attribute: str, old: str, new: dict, kwargs: dict, ) -> None: """Create OmniFocus todos whenever there's a low battery.""" name = new["attributes"]["friendly_name"] try: value = int(new["state"]) except ValueError: # If the sensor value can't be parsed as an integer, it is either a binary # battery sensor or the sensor is unavailable. The former should continue # on; the latter should stop immediately: if new["state"] != "on": return value = 0 notification_handle = "{0}_{1}".format(HANDLE_BATTERY_LOW, name) def _send_notification(): """Send the notification.""" self.handles[notification_handle] = send_notification( self, "slack", "{0} has low batteries ({1}%). Replace them ASAP!".format( name, value), when=self.datetime(), interval=self.properties[CONF_NOTIFICATION_INTERVAL], ) if value < self.properties[CONF_BATTERY_LEVEL_THRESHOLD]: # If we've already registered that the battery is low, don't repeatedly # register it: if name in self._registered: return self.log("Low battery detected: {0}".format(name)) self._registered.append(name) # If the automation is enabled when a battery is low, send a notification; # if not, remember that we should send the notification when the automation # becomes enabled: if self.enabled: _send_notification() else: self._send_notification_func = _send_notification else: try: self._registered.remove(name) self._send_notification_func = None if notification_handle in self.handles: cancel = self.handles.pop(notification_handle) cancel() except ValueError: return def on_enable(self) -> None: """Send the notification once the automation is enabled (if appropriate).""" if self._send_notification_func: self._send_notification_func() self._send_notification_func = None
class NotifyDone(Base): """Define a feature to notify a target when the appliancer is done.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_CLEAN_THRESHOLD): float, vol.Required(CONF_DRYING_THRESHOLD): float, vol.Required(CONF_IOS_EMPTIED_KEY): str, vol.Required(CONF_NOTIFICATION_INTERVAL): int, vol.Required(CONF_RUNNING_THRESHOLD): float, }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.listen_ios_event(self.response_from_push_notification, self.properties[CONF_IOS_EMPTIED_KEY]) self.listen_state(self.power_changed, self.app.entity_ids[CONF_POWER], constrain_input_boolean=self.enabled_entity_id) self.listen_state(self.status_changed, self.app.entity_ids[CONF_STATUS], constrain_input_boolean=self.enabled_entity_id) def power_changed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Deal with changes to the power draw.""" power = float(new) if (self.app.state != self.app.States.running and power >= self.properties[CONF_RUNNING_THRESHOLD]): self.log('Setting dishwasher to "Running"') self.app.state = (self.app.States.running) elif (self.app.state == self.app.States.running and power <= self.properties[CONF_DRYING_THRESHOLD]): self.log('Setting dishwasher to "Drying"') self.app.state = (self.app.States.drying) elif (self.app.state == self.app.States.drying and power == self.properties[CONF_CLEAN_THRESHOLD]): self.log('Setting dishwasher to "Clean"') self.app.state = (self.app.States.clean) def status_changed(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Deal with changes to the status.""" if new == self.app.States.clean.value: self.handles[HANDLE_CLEAN] = self.notification_manager.repeat( "Empty it now and you won't have to do it later!", self.properties[CONF_NOTIFICATION_INTERVAL], title='Dishwasher Clean 🍽', when=self.datetime() + timedelta(minutes=15), target='home', data={'push': { 'category': 'dishwasher' }}) elif old == self.app.States.clean.value: if HANDLE_CLEAN in self.handles: self.handles.pop(HANDLE_CLEAN)() # type: ignore def response_from_push_notification(self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to iOS notification to empty the appliance.""" self.app.state = self.app.States.dirty target = self.notification_manager.get_target_from_push_id( data['sourceDevicePermanentID']) self.notification_manager.send( '{0} emptied the dishwasher.'.format(target.first_name), title='Dishwasher Emptied 🍽', target='not {0}'.format(target.first_name))
class NotifyDone(Base): # pylint: disable=too-few-public-methods """Define a feature to notify a target when the appliancer is done.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_CLEAN_THRESHOLD): float, vol.Required(CONF_DRYING_THRESHOLD): float, vol.Required(CONF_IOS_EMPTIED_KEY): str, vol.Required(CONF_NOTIFICATION_INTERVAL): int, vol.Required(CONF_RUNNING_THRESHOLD): float, }, extra=vol.ALLOW_EXTRA, ) } ) def configure(self) -> None: """Configure.""" if self.enabled and self.app.state == self.app.States.clean: self._start_notification_cycle() self.listen_state(self._on_power_change, self.app.entity_ids[CONF_POWER]) self.listen_state(self._on_status_change, self.app.entity_ids[CONF_STATUS]) def _cancel_notification_cycle(self) -> None: """Cancel any active notification.""" if HANDLE_CLEAN in self.handles: cancel = self.handles.pop(HANDLE_CLEAN) cancel() def _on_power_change( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Deal with changes to the power draw.""" power = float(new) if ( self.app.state != self.app.States.running and power >= self.properties[CONF_RUNNING_THRESHOLD] ): self.log('Setting dishwasher to "Running"') self.app.state = self.app.States.running elif ( self.app.state == self.app.States.running and power <= self.properties[CONF_DRYING_THRESHOLD] ): self.log('Setting dishwasher to "Drying"') self.app.state = self.app.States.drying elif ( self.app.state == self.app.States.drying and power == self.properties[CONF_CLEAN_THRESHOLD] ): self.log('Setting dishwasher to "Clean"') self.app.state = self.app.States.clean def _on_status_change( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Deal with changes to the status.""" if self.enabled and new == self.app.States.clean.value: self._start_notification_cycle() elif old == self.app.States.clean.value: self._cancel_notification_cycle() def _start_notification_cycle(self) -> None: """Start the repeating notification sequence.""" self._cancel_notification_cycle() self.handles[HANDLE_CLEAN] = send_notification( self, "presence:home", "Empty it now and you won't have to do it later!", title="Dishwasher Clean 🍽", when=self.datetime() + timedelta(minutes=15), interval=self.properties[CONF_NOTIFICATION_INTERVAL], data={"push": {"category": "dishwasher"}}, ) def on_disable(self) -> None: """Stop notifying when the automation is disabled.""" self._cancel_notification_cycle() def on_enable(self) -> None: """Start notifying when the automation is enabled (if appropriate).""" if self.app.state == self.app.States.clean: self._start_notification_cycle()
class LowMoisture(Base): """Define a feature to notify us of low moisture.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITY_IDS: vol.Schema( {vol.Required(CONF_CURRENT_MOISTURE): cv.entity_id}, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_FRIENDLY_NAME): str, vol.Required(CONF_MOISTURE_THRESHOLD): int, vol.Required(CONF_NOTIFICATION_INTERVAL): int, }, extra=vol.ALLOW_EXTRA, ), } ) def configure(self) -> None: """Configure.""" self._low_moisture_detected = False self.listen_state( self._on_moisture_change, self.entity_ids[CONF_CURRENT_MOISTURE], constrain_enabled=True, ) @property def current_moisture(self) -> int: """Define a property to get the current moisture.""" try: return int(self.get_state(self.entity_ids[CONF_CURRENT_MOISTURE])) except ValueError: return 100 def _cancel_notification_cycle(self) -> None: """Cancel any active notification.""" if HANDLE_LOW_MOISTURE in self.handles: cancel = self.handles.pop(HANDLE_LOW_MOISTURE) cancel() def _on_moisture_change( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict ) -> None: """Notify when the plant's moisture is low.""" if self.enabled and int(new) < self.properties[CONF_MOISTURE_THRESHOLD]: if self._low_moisture_detected: return self.log("{0} has low moisture".format(self.properties[CONF_FRIENDLY_NAME])) self._start_notification_cycle() self._low_moisture_detected = True elif self.enabled and int(new) >= self.properties[CONF_MOISTURE_THRESHOLD]: if not self._low_moisture_detected: return self._cancel_notification_cycle() self._low_moisture_detected = False def _start_notification_cycle(self) -> None: """Start a repeating notification.""" self.handles[HANDLE_LOW_MOISTURE] = send_notification( self, "presence:home", "{0} is at {1}% moisture and needs water.".format( self.properties[CONF_FRIENDLY_NAME], self.current_moisture ), title="{0} is Dry 💧".format(self.properties[CONF_FRIENDLY_NAME]), when=self.datetime(), interval=self.properties[CONF_NOTIFICATION_INTERVAL], ) def on_disable(self) -> None: """Stop notifications (as necessary) when the automation is disabled.""" self._cancel_notification_cycle() def on_enable(self) -> None: """Start notifications (as necessary) when the automation is enabled.""" try: if self.current_moisture < self.properties[CONF_MOISTURE_THRESHOLD]: self._start_notification_cycle() except TypeError: self.error("Can't parse non-integer moisture level")
class GarageLeftOpen(Base): """Define a feature to notify us when the garage is left open.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_GARAGE_DOOR): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_NOTIFICATION_INTERVAL): int, vol.Required(CONF_TIME_LEFT_OPEN): int, }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.listen_event( self.response_from_push_notification, 'ios.notification_action_fired', actionName='GARAGE_CLOSE', constrain_input_boolean=self.enabled_entity_id) self.listen_state( self.closed, self.entity_ids[CONF_GARAGE_DOOR], new='closed', constrain_input_boolean=self.enabled_entity_id) self.listen_state( self.left_open, self.entity_ids[CONF_GARAGE_DOOR], new='open', duration=self.properties[CONF_TIME_LEFT_OPEN], constrain_input_boolean=self.enabled_entity_id) def closed( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Cancel notification when the garage is closed.""" if HANDLE_GARAGE_OPEN in self.handles: self.handles.pop(HANDLE_GARAGE_OPEN)() # type: ignore def left_open( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send notifications when the garage has been left open.""" message = 'The garage has been left open. Want to close it?' self.handles[HANDLE_GARAGE_OPEN] = self.notification_manager.repeat( message, self.properties[CONF_NOTIFICATION_INTERVAL], title='Garage Open 🚗', blackout_start_time=None, blackout_end_time=None, target=['everyone'], data={'push': { 'category': 'garage' }}) self.slack_app_home_assistant.ask( message, { 'Yes': { 'callback': self.security_manager.close_garage, 'response_text': 'You got it; closing it now.' }, 'No': { 'response_text': 'If you really say so...' } }, urgent=True) def response_from_push_notification( self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to 'ios.notification_action_fired' events.""" self.security_manager.close_garage() target = self.notification_manager.get_target_from_push_id( data['sourceDevicePermanentID']) self.notification_manager.send( '{0} closed the garage.'.format(target.first_name), title='Issue Resolved 🚗', target=['not {0}'.format(target.first_name), 'slack'])
class NotifyWhenStuck(Base): """Define a feature to notify when the vacuum is stuck.""" APP_SCHEMA = APP_SCHEMA.extend( {vol.Required(CONF_NOTIFICATION_INTERVAL_SLIDER): cv.entity_id}) def configure(self) -> None: """Configure.""" if self.enabled and self.app.state == self.app.States.error: self._start_notification_cycle() self.listen_state(self._on_error_change, self.app.args[CONF_VACUUM]) self.listen_state( self._on_notification_interval_change, self.args[CONF_NOTIFICATION_INTERVAL_SLIDER], ) def _cancel_notification_cycle(self) -> None: """Cancel any active notification.""" if HANDLE_STUCK in self.data: cancel = self.data.pop(HANDLE_STUCK) cancel() def _on_error_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Notify when the vacuum is an error state.""" if self.enabled and new == self.app.States.error.value: self._start_notification_cycle() elif old == self.app.States.error.value: self._cancel_notification_cycle() def _on_notification_interval_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Reset the notification interval.""" self._cancel_notification_cycle() if self.enabled and self.app.state == self.app.States.error: self._start_notification_cycle() def _start_notification_cycle(self) -> None: """Start a repeating notification sequence.""" self._cancel_notification_cycle() self.data[HANDLE_STUCK] = send_notification( self, "presence:home", "Help him get back on track or home.", title="Wolfie Stuck 😢", when=self.datetime(), interval=int( float( self.get_state( self.args[CONF_NOTIFICATION_INTERVAL_SLIDER]))) * 60 * 60, data={"push": { "category": "dishwasher" }}, ) def on_disable(self) -> None: """Stop notifying when the automation is disabled.""" self._cancel_notification_cycle() def on_enable(self) -> None: """Start notifying when the automation is enabled (if appropriate).""" if self.app.state == self.app.States.error: self._start_notification_cycle()
class SecurityManager(Base): """Define a class to represent the app.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_ALARM_CONTROL_PANEL): cv.entity_id, vol.Required(CONF_GARAGE_DOOR): cv.entity_id, vol.Required(CONF_OVERALL_SECURITY_STATUS): cv.entity_id, }, extra=vol.ALLOW_EXTRA), }) class AlarmStates(Enum): """Define an enum for alarm states.""" away = 'armed_away' disarmed = 'disarmed' home = 'armed_home' @property def alarm_state(self) -> "AlarmStates": """Return the current state of the security system.""" return self.AlarmStates( self.get_state(self.entity_ids[CONF_ALARM_CONTROL_PANEL])) @property def secure(self) -> bool: """Return whether the house is secure or not.""" return self.get_state( self.entity_ids[CONF_OVERALL_SECURITY_STATUS]) == 'Secure' def configure(self) -> None: """Configure.""" self.listen_state( self._security_system_change_cb, self.entity_ids[CONF_ALARM_CONTROL_PANEL]) def _security_system_change_cb( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Fire events when the security system status changes.""" if new != 'unknown': self.fire_event('ALARM_CHANGE', state=new) def close_garage(self) -> None: """Close the garage.""" self.log('Closing the garage door') self.call_service( 'cover/close_cover', entity_id=self.entity_ids[CONF_GARAGE_DOOR]) def get_insecure_entities(self) -> list: """Return a list of insecure entities.""" return [ entity[CONF_FRIENDLY_NAME] for entity in self.properties['secure_status_mapping'] if self.get_state(entity['entity_id']) == entity[CONF_STATE] ] def open_garage(self) -> None: """Open the garage.""" self.log('Closing the garage door') self.call_service( 'cover.open_cover', entity_id=self.entity_ids[CONF_GARAGE_DOOR]) def set_alarm(self, new: "AlarmStates") -> None: """Set the security system.""" if new == self.AlarmStates.disarmed: self.log('Disarming the security system') self.call_service( 'alarm_control_panel/alarm_disarm', entity_id=self.entity_ids[CONF_ALARM_CONTROL_PANEL]) elif new in (self.AlarmStates.away, self.AlarmStates.home): self.log('Arming the security system: "{0}"'.format(new.name)) self.call_service( 'alarm_control_panel/alarm_arm_{0}'.format( new.value.split('_')[1]), entity_id=self.entity_ids[CONF_ALARM_CONTROL_PANEL]) else: raise AttributeError("Unknown security state: {0}".format(new))
class VacationMode(BaseSwitch): """Define a feature to simulate craziness when we're out of town.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_SWITCH): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_START_TIME): vol.Any(str, vol.In(SOLAR_EVENTS)), vol.Required(CONF_END_TIME): vol.Any(str, vol.In(SOLAR_EVENTS)), }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.set_schedule(self.properties[CONF_START_TIME], self.start_cycle) self.set_schedule(self.properties[CONF_END_TIME], self.stop_cycle) def set_schedule(self, time: str, handler: Callable) -> None: """Set the appropriate schedulers based on the passed in time.""" if time in ('sunrise', 'sunset'): method = getattr(self, 'run_at_{0}'.format(time)) method(handler, constrain_input_boolean=self.enabled_entity_id) else: self.run_daily(handler, self.parse_time(time), constrain_input_boolean=self.enabled_entity_id) def start_cycle(self, kwargs: dict) -> None: """Start the toggle cycle.""" self.toggle_and_run({'state': 'on'}) def stop_cycle(self, kwargs: dict) -> None: """Stop the toggle cycle.""" if HANDLE_VACATION_MODE not in self.handles: return handle = self.handles.pop(HANDLE_VACATION_MODE) self.cancel_timer(handle) self.toggle(state='off') def toggle_and_run(self, kwargs: dict) -> None: """Toggle the swtich and randomize the next toggle.""" self.toggle(state=kwargs[CONF_STATE]) if kwargs[CONF_STATE] == 'on': state = 'off' else: state = 'on' self.handles[HANDLE_VACATION_MODE] = self.run_in(self.toggle_and_run, randint( 5 * 60, 60 * 60), state=state)
class SecurityManager(Base): """Define a class to represent the app.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_ALARM_CONTROL_PANEL): cv.entity_id, vol.Required(CONF_GARAGE_DOOR): cv.entity_id, vol.Required(CONF_OVERALL_SECURITY_STATUS): cv.entity_id, }) class AlarmStates(Enum): """Define an enum for alarm states.""" away = "armed_away" disarmed = "disarmed" home = "armed_home" @property def alarm_state(self) -> "AlarmStates": """Return the current state of the security system.""" return self.AlarmStates( self.get_state(self.args[CONF_ALARM_CONTROL_PANEL])) @property def secure(self) -> bool: """Return whether the house is secure or not.""" return self.get_state( self.args[CONF_OVERALL_SECURITY_STATUS]) == "Secure" def configure(self) -> None: """Configure.""" self.listen_state(self._on_security_system_change, self.args[CONF_ALARM_CONTROL_PANEL]) def _on_security_system_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Fire events when the security system status changes.""" if new != "unknown": self.fire_event(EVENT_ALARM_CHANGE, state=new) def close_garage(self) -> None: """Close the garage.""" self.log("Closing the garage door") self.call_service("cover/close_cover", entity_id=self.args[CONF_GARAGE_DOOR]) def get_insecure_entities(self) -> list: """Return a list of insecure entities.""" return [ entity[CONF_FRIENDLY_NAME] for entity in self.args["secure_status_mapping"] if self.get_state(entity["entity_id"]) == entity[CONF_STATE] ] def open_garage(self) -> None: """Open the garage.""" self.log("Closing the garage door") self.call_service("cover.open_cover", entity_id=self.args[CONF_GARAGE_DOOR]) def set_alarm(self, new: "AlarmStates") -> None: """Set the security system.""" if new == self.AlarmStates.disarmed: self.log("Disarming the security system") self.call_service( "alarm_control_panel/alarm_disarm", entity_id=self.args[CONF_ALARM_CONTROL_PANEL], ) elif new in (self.AlarmStates.away, self.AlarmStates.home): self.log('Arming the security system: "%s"', new.name) self.call_service( f'alarm_control_panel/alarm_arm_{new.value.split("_")[1]}', entity_id=self.args[CONF_ALARM_CONTROL_PANEL], ) else: raise AttributeError(f"Unknown security state: {new}")
class ClimateManager(Base): """Define an app to represent climate control.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_AVG_HUMIDITY_SENSOR): cv.entity_id, vol.Required(CONF_AVG_TEMP_SENSOR): cv.entity_id, vol.Required(CONF_OUTSIDE_TEMP): cv.entity_id, vol.Required(CONF_THERMOSTAT): cv.entity_id, }, extra=vol.ALLOW_EXTRA), }) class AwayModes(Enum): """Define an enum for thermostat away modes.""" away = 1 home = 2 class FanModes(Enum): """Define an enum for thermostat fan modes.""" auto = 1 on = 2 class Modes(Enum): """Define an enum for thermostat modes.""" auto = 1 cool = 2 eco = 3 heat = 4 off = 5 @property def average_indoor_humidity(self) -> float: """Return the average indoor humidity based on a list of sensors.""" return float(self.get_state(self.entity_ids[CONF_AVG_HUMIDITY_SENSOR])) @property def average_indoor_temperature(self) -> float: """Return the average indoor temperature based on a list of sensors.""" return float(self.get_state(self.entity_ids[CONF_AVG_TEMP_SENSOR])) @property def away_mode(self) -> bool: """Return the state of away mode.""" return self.get_state(self.entity_ids[CONF_THERMOSTAT], attribute='away_mode') == 'on' @property def indoor_temp(self) -> int: """Return the temperature the thermostat is currently set to.""" try: return int( self.get_state(self.entity_ids[CONF_THERMOSTAT], attribute='temperature')) except TypeError: return 0 @property def mode(self) -> Enum: """Return the current operating mode.""" return self.Modes[self.get_state(self.entity_ids[CONF_THERMOSTAT], attribute='operation_mode')] @property def outside_temp(self) -> float: """Define a property to get the current outdoor temperature.""" return float(self.get_state(self.entity_ids[CONF_OUTSIDE_TEMP])) def configure(self) -> None: """Configure.""" self.register_endpoint(self._climate_bump_endpoint, 'climate_bump') def _climate_bump_endpoint(self, data: dict) -> Tuple[dict, int]: """Define an endpoint to quickly bump the climate.""" if not data.get('amount'): return { 'status': 'error', 'message': 'Missing "amount" parameter' }, 502 self.bump_indoor_temp(int(data['amount'])) return { "status": "ok", "message": 'Bumping temperature {0}°'.format(data['amount']) }, 200 def bump_indoor_temp(self, value: int) -> None: """Bump the current temperature.""" self.set_indoor_temp(self.indoor_temp + value) def set_away_mode(self, value: "AwayModes") -> None: """Set the state of away mode.""" self.call_service('nest/set_away_mode', away_mode=value.name) def set_indoor_temp(self, value: int) -> None: """Set the thermostat temperature.""" self.call_service('climate/set_temperature', entity_id=self.entity_ids[CONF_THERMOSTAT], temperature=str(value)) def set_fan_mode(self, value: Enum) -> None: """Set the themostat's fan mode.""" self.call_service('climate/set_fan_mode', entity_id=self.entity_ids[CONF_THERMOSTAT], fan_mode=value.name) def set_mode(self, value: Enum) -> None: """Set the themostat's operating mode.""" self.call_service('climate/set_operation_mode', entity_id=self.entity_ids[CONF_THERMOSTAT], operation_mode=value.name)
from core import APP_SCHEMA, Base from const import CONF_FRIENDLY_NAME, CONF_ICON, CONF_INTERVAL from helpers import config_validation as cv from notification import send_notification CONF_APP_NAME = "app_name" CONF_AVAILABLE = "available" CONF_CREATED_ENTITY_ID = "created_entity_id" CONF_ENDPOINT_ID = "endpoint_id" CONF_IMAGE_NAME = "image_name" CONF_INSTALLED = "installed" CONF_VERSION_SENSORS = "version_sensors" VERSION_APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_AVAILABLE): cv.entity_id, vol.Required(CONF_INSTALLED): cv.entity_id, vol.Required(CONF_APP_NAME): cv.string, }) DYNAMIC_APP_SCHEMA = VERSION_APP_SCHEMA.extend({ vol.Required(CONF_CREATED_ENTITY_ID): cv.entity_id, vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_ICON): cv.icon, vol.Required(CONF_INTERVAL): vol.All(cv.time_period, lambda value: value.seconds), })
class ScheduledCycle(Base): """Define a feature to run the vacuum on a schedule.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_IOS_EMPTIED_KEY): str, vol.Required(CONF_NOTIFICATION_INTERVAL_FULL): int, vol.Required(CONF_NOTIFICATION_INTERVAL_STUCK): int, vol.Required(CONF_SCHEDULE_SWITCHES): cv.ensure_list, vol.Required(CONF_SCHEDULE_TIME): str, }, extra=vol.ALLOW_EXTRA), }) @property def active_days(self) -> list: """Get the days that the vacuuming schedule should run.""" on_days = [] for toggle in self.properties['schedule_switches']: state = self.get_state(toggle, attribute='all') if state['state'] == 'on': on_days.append(state['attributes']['friendly_name']) return on_days def configure(self) -> None: """Configure.""" self.initiated_by_app = False self.create_schedule() self.listen_event( self.alarm_changed, 'ALARM_CHANGE', constrain_input_boolean=self.enabled_entity_id) self.listen_event( self.start_by_switch, 'VACUUM_START', constrain_input_boolean=self.enabled_entity_id) self.listen_ios_event( self.response_from_push_notification, self.properties['ios_emptied_key']) self.listen_state( self.all_done, self.app.entity_ids['status'], old=self.app.States.returning.value, new=self.app.States.docked.value, constrain_input_boolean=self.enabled_entity_id) self.listen_state( self.bin_state_changed, self.app.entity_ids['bin_state'], constrain_input_boolean=self.enabled_entity_id) self.listen_state( self.errored, self.app.entity_ids['status'], new=self.app.States.error.value, constrain_input_boolean=self.enabled_entity_id) self.listen_state( self.error_cleared, self.app.entity_ids['status'], old=self.app.States.error.value, constrain_input_boolean=self.enabled_entity_id) for toggle in self.properties['schedule_switches']: self.listen_state( self.schedule_changed, toggle, constrain_input_boolean=self.enabled_entity_id) def alarm_changed(self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to 'ALARM_CHANGE' events.""" state = self.app.States(self.get_state(self.app.entity_ids['status'])) # Scenario 1: Vacuum is charging and is told to start: if ((self.initiated_by_app and state == self.app.States.docked) and data['state'] == self.security_manager.AlarmStates.home.value): self.log('Activating vacuum (post-security)') self.turn_on(self.app.entity_ids['vacuum']) # Scenario 2: Vacuum is running when alarm is set to "Away": elif (state == self.app.States.cleaning and data['state'] == self.security_manager.AlarmStates.away.value): self.log('Security mode is "Away"; pausing until "Home"') self.call_service( 'vacuum/start_pause', entity_id=self.app.entity_ids['vacuum']) self.security_manager.set_alarm( self.security_manager.AlarmStates.home) # Scenario 3: Vacuum is paused when alarm is set to "Home": elif (state == self.app.States.paused and data['state'] == self.security_manager.AlarmStates.home.value): self.log('Alarm in "Home"; resuming') self.call_service( 'vacuum/start_pause', entity_id=self.app.entity_ids['vacuum']) def all_done( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Re-arm security (if needed) when done.""" self.log('Vacuuming cycle all done') if self.presence_manager.noone( self.presence_manager.HomeStates.just_arrived, self.presence_manager.HomeStates.home): self.log('Changing alarm state to "away"') self.security_manager.set_alarm( self.security_manager.AlarmStates.away) self.app.bin_state = self.app.BinStates.full self.initiated_by_app = False def bin_state_changed( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Listen for changes in bin status.""" if new == self.app.BinStates.full.value: self.handles[HANDLE_BIN] = self.notification_manager.repeat( "Empty him now and you won't have to do it later!", self.properties[CONF_NOTIFICATION_INTERVAL_FULL], title='Wolfie Full 🤖', target='home', data={'push': { 'category': 'wolfie' }}) elif new == self.app.BinStates.empty.value: if HANDLE_BIN in self.handles: self.handles.pop(HANDLE_BIN)() # type: ignore def create_schedule(self) -> None: """Create the vacuuming schedule from the on booleans.""" if HANDLE_SCHEDULE in self.handles: for handle in self.handles.pop(HANDLE_SCHEDULE): self.cancel_timer(handle) self.handles[HANDLE_SCHEDULE] = run_on_days( # type: ignore self, self.start_by_schedule, self.active_days, self.parse_time(self.properties['schedule_time']), constrain_input_boolean=self.enabled_entity_id) def error_cleared( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Clear the error when Wolfie is no longer stuck.""" if HANDLE_STUCK in self.handles: self.handles.pop(HANDLE_STUCK)() # type: ignore def errored( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Brief when Wolfie's had an error.""" self.handles[HANDLE_STUCK] = self.notification_manager.repeat( "Help him get back on track or home.", self.properties['notification_interval_stuck'], title='Wolfie Stuck 😢', target='home', ) def response_from_push_notification( self, event_name: str, data: dict, kwargs: dict) -> None: """Respond to iOS notification to empty vacuum.""" self.app.bin_state = (self.app.BinStates.empty) target = self.notification_manager.get_target_from_push_id( data['sourceDevicePermanentID']) self.notification_manager.send( '{0} emptied the vacuum.'.format(target.first_name), title='Vacuum Emptied 🤖', target='not {0}'.format(target.first_name)) def schedule_changed( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Reload the schedule when one of the input booleans change.""" self.create_schedule() def start_by_schedule(self, kwargs: dict) -> None: """Start cleaning via the schedule.""" if not self.initiated_by_app: self.app.start() self.initiated_by_app = True def start_by_switch( self, event_name: str, data: dict, kwargs: dict) -> None: """Start cleaning via the switch.""" if not self.initiated_by_app: self.app.start() self.initiated_by_app = True