class SleepTimer(BaseSwitch): """Define a feature to turn a switch off after an amount of time.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_SWITCH): cv.entity_id, vol.Required(CONF_TIMER_SLIDER): cv.entity_id, }, extra=vol.ALLOW_EXTRA, ) }) def configure(self) -> None: """Configure.""" self.listen_state( self._on_switch_turned_off, self.entity_ids[CONF_SWITCH], new="off", constrain_enabled=True, ) self.listen_state( self._on_timer_change, self.entity_ids[CONF_TIMER_SLIDER], constrain_enabled=True, ) def _on_timer_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Start/stop a sleep timer for this switch.""" minutes = int(float(new)) if minutes == 0: self.log("Deactivating sleep timer") self.toggle(state="off") if HANDLE_TIMER in self.handles: cancel = self.handles.pop(HANDLE_TIMER) self.cancel_timer(cancel) else: self.log("Activating sleep timer: {0} minutes".format(minutes)) self.toggle(state="on") self.handles[HANDLE_TIMER] = self.run_in(self._on_timer_complete, minutes * 60) def _on_switch_turned_off(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Reset the sleep timer when the switch turns off.""" self.set_value(self.entity_ids[CONF_TIMER_SLIDER], 0) def _on_timer_complete(self, kwargs: dict) -> None: """Turn off a switch at the end of sleep timer.""" self.log("Sleep timer over; turning switch off") self.set_value(self.entity_ids[CONF_TIMER_SLIDER], 0)
class BadLoginNotification(Base): # pylint: disable=too-few-public-methods """Define a feature to notify me of unauthorized login attempts.""" APP_SCHEMA = APP_SCHEMA.extend( { CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_BAD_LOGIN): cv.entity_id, vol.Required(CONF_IP_BAN): cv.entity_id, }, extra=vol.ALLOW_EXTRA, ) } ) def configure(self) -> None: """Configure.""" self._send_notification_func = None # type: Optional[Callable] for notification_type in self.entity_ids.values(): self.listen_state( self._on_bad_login, notification_type, attribute="all", constrain_enabled=True, ) def _on_bad_login( self, entity: str, attribute: str, old: str, new: dict, kwargs: dict ) -> None: """Send a notification when there's a bad login attempt.""" if not new: return if entity == self.entity_ids[CONF_BAD_LOGIN]: title = "Unauthorized Access Attempt" else: title = "IP Ban" def _send_notification() -> None: """Send a notification about the attempt.""" send_notification( self, "person:Aaron", new["attributes"]["message"], title=title ) 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 self._send_notification_func: self._send_notification_func() self._send_notification_func = None
class NotifyWhenRunComplete(Base): """Define a feature to notify when the vacuum cycle is complete.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({vol.Required(CONF_NOTIFICATION_INTERVAL): int}, extra=vol.ALLOW_EXTRA) }) def configure(self) -> None: """Configure.""" if self.enabled and self.app.bin_state == self.app.BinStates.full: self._start_notification_cycle() self.listen_state(self._on_vacuum_bin_change, self.app.entity_ids[CONF_BIN_STATE]) def _cancel_notification_cycle(self) -> None: """Cancel any active notification.""" if HANDLE_BIN_FULL in self.handles: cancel = self.handles.pop(HANDLE_BIN_FULL) cancel() def _on_vacuum_bin_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Deal with changes to the bin.""" if self.enabled and new == self.app.BinStates.full.value: self._start_notification_cycle() elif old == self.app.BinStates.full.value: self._cancel_notification_cycle() def _start_notification_cycle(self) -> None: """Start a repeating notification sequence.""" self._cancel_notification_cycle() self.handles[HANDLE_BIN_FULL] = send_notification( self, "presence:home", "Empty him now and you won't have to do it later!", title="Wolfie Full 🤖", when=self.datetime(), 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.bin_state == self.app.BinStates.full: self._start_notification_cycle()
class NotifyWhenStuck(Base): """Define a feature to notify when the vacuum is stuck.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_PROPERTIES: vol.Schema({vol.Required(CONF_NOTIFICATION_INTERVAL): int}, extra=vol.ALLOW_EXTRA) }) 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.entity_ids[CONF_STATUS]) def _cancel_notification_cycle(self) -> None: """Cancel any active notification.""" if HANDLE_STUCK in self.handles: cancel = self.handles.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 _start_notification_cycle(self) -> None: """Start a repeating notification sequence.""" self._cancel_notification_cycle() self.handles[HANDLE_STUCK] = send_notification( self, "presence:home", "Help him get back on track or home.", title="Wolfie Stuck 😢", when=self.datetime(), 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.error: self._start_notification_cycle()
class NewPortainerVersionNotification(DynamicSensor): """Detect new versions of Portainer-defined images.""" APP_SCHEMA = DYNAMIC_APP_SCHEMA.extend({ vol.Required(CONF_PROPERTIES): PROPERTIES_SCHEMA.extend({ vol.Required(CONF_ENDPOINT_ID): int, vol.Required(CONF_IMAGE_NAME): str, }) }) API_URL = 'http://portainer:9000/api' APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_AVAILABLE): cv.entity_id, vol.Required(CONF_INSTALLED): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_APP_NAME): str, }, extra=vol.ALLOW_EXTRA), }) @property def sensor_value(self) -> Optional[str]: """Get the version from Portainer.""" auth_resp = requests.post('{0}/auth'.format(self.API_URL), json={ 'Username': self.config['portainer_username'], 'Password': self.config['portainer_password'] }).json() token = auth_resp['jwt'] images_resp = requests.get( '{0}/endpoints/{1}/docker/images/json'.format( self.API_URL, self.properties[CONF_ENDPOINT_ID]), headers={ 'Authorization': 'Bearer {0}'.format(token) }).json() try: tagged_image = next((i for image in images_resp for i in image['RepoTags'] if self.properties[CONF_IMAGE_NAME] in i)) except StopIteration: self.error('No match for image: {0}'.format( self.properties[CONF_IMAGE_NAME])) return tagged_image.split(':')[1].replace('v', '').split('-')[0]
class MonitorConsumables(Base): # pylint: disable=too-few-public-methods """Define a feature to notify when a consumable gets low.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_CONSUMABLE_THRESHOLD): cv.positive_int, vol.Required(CONF_CONSUMABLES): vol.All(cv.ensure_list, [cv.string]), }) def configure(self) -> None: """Configure.""" self._consumables_met: List[str] = [] self._send_notification_func: Optional[Callable] = None for consumable in self.args[CONF_CONSUMABLES]: self.listen_state( self._on_consumable_change, self.app.args[CONF_VACUUM], attribute=consumable, ) def _on_consumable_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Create a task when a consumable is getting low.""" def _send_notification() -> None: """Send the notification.""" send_notification(self, "slack:@aaron", f"Order a new Wolfie consumable: {attribute}") if int(new) < self.args[CONF_CONSUMABLE_THRESHOLD]: if attribute in self._consumables_met: return self._consumables_met.append(attribute) self.log("Consumable is low: %s", attribute) if self.enabled: _send_notification() else: self._send_notification_func = _send_notification else: if attribute not in self._consumables_met: return self._consumables_met.remove(attribute) self.log("Consumable is restored: %s", attribute) 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 AbsentInsecure(Base): # pylint: disable=too-few-public-methods """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._send_notification_func = None # type: Optional[Callable] self.listen_state( self._on_house_insecure, self.entity_ids[CONF_STATE], new="Open", duration=60 * 5, constrain_enabled=True, constrain_noone="just_arrived,home", ) def _on_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.""" def _send_notification() -> None: """Send the notification.""" send_notification( self, ["person:Aaron", "person:Britt"], "No one is home and the house isn't locked up.", title="Security Issue 🔐", data={"push": {"category": "dishwasher"}}, ) self.log("No one home and house is insecure; notifying") # If the automation is enabled when the house is insecure, 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 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 Vacuum(Base): """Define an app to represent a vacuum-type appliance.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_BIN_STATE): cv.entity_id, vol.Required(CONF_STATUS): cv.entity_id, vol.Required(CONF_VACUUM): cv.entity_id, }, extra=vol.ALLOW_EXTRA), }) @property def bin_state(self) -> Enum: """Define a property to get the bin state.""" return self.BinStates(self.get_state(self.entity_ids['bin_state'])) @bin_state.setter def bin_state(self, value: Enum) -> None: """Set the bin state.""" self.select_option(self.entity_ids['bin_state'], value.value) 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' remote_control = 'Remote Control' returning = 'Returning' 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.entity_ids['vacuum'])
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): cv.entity_id, vol.Required(CONF_HVAC_STATE): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_AQI_THRESHOLD): int, }, extra=vol.ALLOW_EXTRA), }) @property def current_aqi(self) -> int: """Define a property to get the current AQI.""" return int(self.get_state(self.entity_ids[CONF_AQI])) def configure(self) -> None: """Configure.""" self.notification_sent = False self.listen_state(self.bad_aqi_detected, self.entity_ids[CONF_HVAC_STATE], new='cooling', constrain_input_boolean=self.enabled_entity_id) def bad_aqi_detected(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send select notifications when cooling and poor AQI.""" if (not self.notification_sent and self.current_aqi > self.properties[CONF_AQI_THRESHOLD]): self.log('Poor AQI; notifying anyone at home') self.notification_manager.send( 'AQI is at {0}; consider closing the humidifier vent.'.format( self.current_aqi), title='Poor AQI 😤', target='home') self.notification_sent = True elif (self.notification_sent and self.current_aqi <= self.properties[CONF_AQI_THRESHOLD]): self.notification_manager.send( 'AQI is at {0}; open the humidifer vent again.'.format( self.current_aqi), title='Better AQI 😅', target='home') self.notification_sent = True
class NotifyLowFuel(Base): # pylint: disable=too-few-public-methods """Define a feature to notify of the vehicle's ETA to home.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({vol.Required(CONF_CAR): cv.entity_id}, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_FRIENDLY_NAME): str, vol.Required(CONF_FUEL_THRESHOLD): int, vol.Required(CONF_NOTIFICATION_TARGET): str, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self): """Configure.""" self.registered = False self.listen_state( self._on_low_fuel, self.entity_ids[CONF_CAR], attribute="fuel_level", constrain_enabled=True, ) def _on_low_fuel(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send a notification when my car is low on gas.""" try: if int(new) < self.properties["fuel_threshold"]: if self.registered: return self.log("Low fuel detected detected: {0}".format( self.entity_ids[CONF_CAR])) self.registered = True send_notification( self, self.properties[CONF_NOTIFICATION_TARGET], "{0} needs gas; fill 'er up!.".format( self.properties[CONF_FRIENDLY_NAME]), title="{0} is Low ⛽".format( self.properties[CONF_FRIENDLY_NAME]), ) else: self.registered = False except ValueError: return
class LeftInState(Base): # pylint: disable=too-few-public-methods """Define a feature to monitor whether an entity is left in a state.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_DURATION): vol.All(cv.time_period, lambda value: value.seconds), vol.Required(CONF_STATE): cv.string, vol.Required(CONF_NOTIFICATION_TARGET): vol.All(cv.ensure_list, [cv.notification_target]), }) def configure(self) -> None: """Configure.""" self._send_notification_func = None # type: Optional[Callable] self.listen_state( self._on_limit, self.args[CONF_ENTITY_ID], new=self.args[CONF_STATE], duration=self.args[CONF_DURATION], ) def _on_limit(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Notify when the threshold is reached.""" def _send_notification() -> None: """Send a notification.""" friendly_name = self.get_state(self.args[CONF_ENTITY_ID], attribute="friendly_name") send_notification( self, self.args[CONF_NOTIFICATION_TARGET], (f"{friendly_name} -> {self.args[CONF_STATE]} " f"({self.args[CONF_DURATION]} seconds)"), title="Entity Alert", ) # If the automation is enabled when the limit is reached, 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 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 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 = False self.listen_state(self.low_moisture_detected, self.entity_ids['current_moisture'], constrain_input_boolean=self.enabled_entity_id) @property def current_moisture(self) -> int: """Define a property to get the current moisture.""" return int(self.get_state(self.entity_ids['current_moisture'])) def low_moisture_detected(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Notify when the plant's moisture is low.""" if (not self._low_moisture and int(new) < int(self.properties['moisture_threshold'])): self.log('Notifying people at home that plant is low on moisture') self._low_moisture = True self.handles[ HANDLE_LOW_MOISTURE] = self.notification_manager.repeat( '{0} is at {1}% moisture and needs water.'.format( self.properties['friendly_name'], self.current_moisture), self.properties['notification_interval'], title='{0} is Dry 💧'.format( self.properties['friendly_name']), target='home') else: self._low_moisture = False if HANDLE_LOW_MOISTURE in self.handles: self.handles.pop(HANDLE_LOW_MOISTURE)() # type: ignore
class ServiceOnZWaveSwitchDoubleTap(Base): # pylint: disable=too-few-public-methods """Define an automation to call a service when a Z-Wave switch double-tap occurs.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_SERVICES): vol.All( vol.Schema({ vol.Inclusive(CONF_SERVICE_UP, "up"): cv.string, vol.Inclusive(CONF_SERVICE_UP_DATA, "up"): dict, vol.Inclusive(CONF_SERVICE_DOWN, "down"): cv.string, vol.Inclusive(CONF_SERVICE_DOWN_DATA, "down"): dict, }), cv.has_at_least_one_key(CONF_SERVICE_UP, CONF_SERVICE_DOWN), ), vol.Required(CONF_ZWAVE_DEVICE): cv.entity_id, }) def configure(self) -> None: """Configure.""" self.listen_event( self._on_double_tap_up, "zwave.node_event", entity_id=self.args[CONF_ZWAVE_DEVICE], basic_level=255, ) self.listen_event( self._on_double_tap_down, "zwave.node_event", entity_id=self.args[CONF_ZWAVE_DEVICE], basic_level=0, ) def _on_double_tap_down(self, event_name: str, data: dict, kwargs: dict) -> None: """Call the "down" service.""" if CONF_SERVICE_DOWN not in self.args[CONF_SERVICES]: self.log("No service defined for double-tap down") return self.call_service(self.args[CONF_SERVICES][CONF_SERVICE_DOWN], **self.args[CONF_SERVICES][CONF_SERVICE_DOWN_DATA]) def _on_double_tap_up(self, event_name: str, data: dict, kwargs: dict) -> None: """Call the "up" service.""" if CONF_SERVICE_UP not in self.args[CONF_SERVICES]: self.log("No service defined for double-tap up") return self.call_service(self.args[CONF_SERVICES][CONF_SERVICE_UP], **self.args[CONF_SERVICES][CONF_SERVICE_UP_DATA])
class NotifyLowFuel(Base): """Define a feature to notify of the vehicle's ETA to home.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_CAR): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_FRIENDLY_NAME): str, vol.Required(CONF_FUEL_THRESHOLD): int, vol.Required(CONF_NOTIFICATION_TARGET): str, }, extra=vol.ALLOW_EXTRA), }) def configure(self): """Configure.""" self.registered = False self.listen_state(self.low_fuel_found, self.entity_ids[CONF_CAR], attribute='fuel_level', constrain_input_boolean=self.enabled_entity_id) def low_fuel_found(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send a notification when my car is low on gas.""" try: if int(new) < self.properties['fuel_threshold']: if self.registered: return self.log('Low fuel detected detected: {0}'.format( self.entity_ids[CONF_CAR])) self.registered = True self.notification_manager.send( "{0} needs gas; fill 'er up!.".format( self.properties[CONF_FRIENDLY_NAME]), title='{0} is Low ⛽'.format( self.properties[CONF_FRIENDLY_NAME]), target=self.properties[CONF_NOTIFICATION_TARGET]) else: self.registered = False except ValueError: return
class ToggleOnNumericThreshold(BaseSwitch): """Define a feature to toggle the switch above/below a threshold.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_SWITCH): cv.entity_id, vol.Required(CONF_TARGET): cv.entity_id, }, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.All( vol.Schema( { vol.Required(CONF_STATE): vol.In(TOGGLE_STATES), vol.Optional(CONF_ABOVE): int, vol.Optional(CONF_BELOW): int, }, extra=vol.ALLOW_EXTRA, ), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), }) def configure(self) -> None: """Configure.""" self.listen_state( self._on_state_change, self.entity_ids[CONF_TARGET], auto_constraints=True, constrain_enabled=True, ) def _on_state_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Toggle the switch if outside the threshold.""" new_value = float(new) above = self.properties.get(CONF_ABOVE) below = self.properties.get(CONF_BELOW) if above and new_value >= above: self.toggle(state=self.properties[CONF_STATE]) elif below and new_value < below: self.toggle(state=self.properties[CONF_STATE]) else: self.toggle(opposite_of=self.properties[CONF_STATE])
class ToggleAtTime(BaseSwitch): """Define a feature to toggle a switch at a certain time.""" 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_SCHEDULE_TIME): vol.Any(str, vol.In(SOLAR_EVENTS)), vol.Required(CONF_STATE): vol.In(TOGGLE_STATES), vol.Optional(CONF_RUN_ON_DAYS): cv.ensure_list, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" kwargs = { "state": self.properties[CONF_STATE], "constrain_enabled": True } if self.properties[CONF_SCHEDULE_TIME] in SOLAR_EVENTS: method = getattr( self, "run_at_{0}".format(self.properties[CONF_SCHEDULE_TIME])) method(self._on_schedule_toggle, auto_constraints=True, **kwargs) else: if self.properties.get(CONF_RUN_ON_DAYS): run_on_days( self, self._on_schedule_toggle, self.properties[CONF_RUN_ON_DAYS], self.parse_time(self.properties[CONF_SCHEDULE_TIME]), auto_constraints=True, **kwargs, ) else: self.run_daily( self._on_schedule_toggle, self.parse_time(self.properties[CONF_SCHEDULE_TIME]), auto_constraints=True, **kwargs, )
class ToggleOnState(BaseSwitch): """Define a feature to toggle the switch when an entity enters a state.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema( { vol.Required(CONF_SWITCH): cv.entity_id, vol.Required(CONF_TARGET): cv.entity_id, }, extra=vol.ALLOW_EXTRA, ), CONF_PROPERTIES: vol.Schema( { vol.Required(CONF_SWITCH_STATE): vol.In(TOGGLE_STATES), vol.Required(CONF_TARGET_STATE): vol.In(TOGGLE_STATES), vol.Optional(CONF_DELAY): int, }, extra=vol.ALLOW_EXTRA, ), }) def configure(self) -> None: """Configure.""" self.listen_state( self._on_state_change, self.entity_ids[CONF_TARGET], auto_constraints=True, constrain_enabled=True, ) def _on_state_change(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Toggle the switch depending on the target entity's state.""" if new == self.properties[CONF_TARGET_STATE]: if self.properties.get(CONF_DELAY): self.handles[HANDLE_TOGGLE_STATE] = self.run_in( self._on_schedule_toggle, self.properties[CONF_DELAY], state=self.properties[CONF_SWITCH_STATE], ) else: self.toggle(state=self.properties[CONF_SWITCH_STATE]) else: if HANDLE_TOGGLE_STATE in self.handles: handle = self.handles.pop(HANDLE_TOGGLE_STATE) self.cancel_timer(handle)
class NewPortainerVersionNotification(DynamicSensor): """Detect new versions of Portainer-defined images.""" APP_SCHEMA = DYNAMIC_APP_SCHEMA.extend({ vol.Required(CONF_ENDPOINT_ID): cv.positive_int, vol.Required(CONF_IMAGE_NAME): cv.string, }) API_URL = "http://portainer:9000/api" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_AVAILABLE): cv.entity_id, vol.Required(CONF_INSTALLED): cv.entity_id, vol.Required(CONF_APP_NAME): str, }) @property def sensor_value(self) -> Optional[str]: """Get the version from Portainer.""" auth_resp = requests.post( f"{self.API_URL}/auth", json={ "Username": self.config["portainer_username"], "Password": self.config["portainer_password"], }, ).json() token = auth_resp["jwt"] images_resp = requests.get( (f"{self.API_URL}/endpoints/{self.args[CONF_ENDPOINT_ID]}" "/docker/images/json"), headers={ "Authorization": f"Bearer {token}" }, ).json() try: tagged_image = next((i for image in images_resp for i in image["RepoTags"] if self.args[CONF_IMAGE_NAME] in i)) except StopIteration: self.error("No match for image: %s", self.args[CONF_IMAGE_NAME]) return None return tagged_image.split(":")[1].replace("v", "").split("-")[0]
class AmazonDashButton(Button): """Define an Amazon Dash button.""" APP_SCHEMA = APP_SCHEMA.extend( { vol.Required(CONF_ACTION_LIST): cv.entity_id, vol.Required(CONF_FRIENDLY_NAME): cv.string, } ) def configure(self) -> None: """Configure.""" self.listen_event( self._on_button_press, "AMAZON_DASH_PRESS", button_label=self.args[CONF_FRIENDLY_NAME], )
class LeftInState(Base): """Define a feature to monitor whether an entity is left in a state.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({ vol.Required(CONF_ENTITY): cv.entity_id, }, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({ vol.Required(CONF_DURATION): int, vol.Required(CONF_STATE): str, }, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.listen_state( self.limit_reached, self.entity_ids[CONF_ENTITY], new=self.properties[CONF_STATE], duration=self.properties[CONF_DURATION], constrain_input_boolean=self.enabled_entity_id) def limit_reached( self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Notify when the threshold is reached.""" def turn_off(): """Turn the entity off.""" self.turn_off(self.entity_ids[CONF_ENTITY]) self.slack_app_home_assistant.ask( 'The {0} has been left {1} for {2} minutes. Turn it off?'.format( self.get_state( self.entity_ids[CONF_ENTITY], attribute='friendly_name'), self.properties[CONF_STATE], int(self.properties[CONF_DURATION]) / 60), { 'Yes': { 'callback': turn_off, 'response_text': 'You got it; turning it off now.' }, 'No': { 'response_text': 'Keep devouring electricity, little guy.' } })
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.run_daily(self.send_notification_when_blackout_start, self.parse_time(BLACKOUT_START)) self.listen_state(self.send_notification_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 _send_notification(self) -> None: """Send notification to my love.""" self.notification_manager.send( "His phone shouldn't be off wifi during the night.", title='Check on Aaron', target='Britt') def send_notification_when_blackout_start(self, kwargs: dict) -> None: """Send a notification if offline when blackout starts.""" if (self.aaron.home_state == self.presence_manager.HomeStates.home and self.router_tracker_state == 'not_home'): self._send_notification() def send_notification_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()
class NotifyLowFuel(Base): # pylint: disable=too-few-public-methods """Define a feature to notify of the vehicle's ETA to home.""" APP_SCHEMA = APP_SCHEMA.extend({ vol.Required(CONF_CAR): cv.entity_id, vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_FUEL_THRESHOLD): cv.positive_int, vol.Required(CONF_NOTIFICATION_TARGET): cv.notification_target, }) def configure(self): """Configure.""" self.registered = False self.listen_state(self._on_low_fuel, self.args[CONF_CAR], attribute="fuel_level") def _on_low_fuel(self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict) -> None: """Send a notification when my car is low on gas.""" try: if int(new) < self.args["fuel_threshold"]: if self.registered: return self.registered = True self.log("Low fuel detected detected: %s", self.args[CONF_CAR]) send_notification( self, self.args[CONF_NOTIFICATION_TARGET], f"{self.args[CONF_FRIENDLY_NAME]} needs gas; fill 'er up!.", title=f"{self.args[CONF_FRIENDLY_NAME]} is low ⛽", ) else: self.registered = False except ValueError: return
class AmazonDashButton(Button): """Define an Amazon Dash button.""" APP_SCHEMA = APP_SCHEMA.extend({ CONF_ENTITY_IDS: vol.Schema({vol.Required(CONF_ACTION_LIST): cv.entity_id}, extra=vol.ALLOW_EXTRA), CONF_PROPERTIES: vol.Schema({vol.Required(CONF_FRIENDLY_NAME): str}, extra=vol.ALLOW_EXTRA), }) def configure(self) -> None: """Configure.""" self.listen_event( self._on_button_press, "AMAZON_DASH_PRESS", button_label=self.properties[CONF_FRIENDLY_NAME], )
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 ZWaveButton(Button): """Define a Z-Wave button.""" APP_SCHEMA = APP_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ACTION_LIST): cv.entity_id, vol.Required(CONF_SCENE_ID): cv.positive_int, vol.Required(CONF_SCENE_DATA): cv.positive_int, } ) def configure(self) -> None: """Configure.""" self.listen_event( self._on_button_press, "zwave.scene_activated", scene_id=self.args[CONF_SCENE_ID], scene_data=self.args[CONF_SCENE_DATA], )
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