Example #1
0
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")
Example #2
0
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
Example #3
0
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))
Example #4
0
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",
        )
Example #5
0
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,
        )
Example #6
0
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
Example #7
0
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])
Example #8
0
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),
                       })
Example #9
0
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
Example #10
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
Example #11
0
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])
Example #12
0
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),
            },
        )
Example #13
0
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):
Example #14
0
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'])
Example #15
0
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()
Example #16
0
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")
Example #17
0
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()
Example #18
0
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
Example #19
0
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
Example #20
0
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))
Example #21
0
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()
Example #22
0
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")
Example #23
0
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'])
Example #24
0
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()
Example #25
0
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))
Example #26
0
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)
Example #27
0
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}")
Example #28
0
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)
Example #29
0
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),
})

Example #30
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_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