mos.ru power sensors
This commit is contained in:
@@ -2,6 +2,9 @@ ARG VERSION
|
||||
FROM acockburn/appdaemon:${VERSION}
|
||||
|
||||
RUN pip3 install \
|
||||
attrs==19.1.0 \
|
||||
emp_mos_api==0.10 \
|
||||
sqlalchemy \
|
||||
mysqlclient
|
||||
packaging==16.8 \
|
||||
python-Levenshtein==0.12.0 \
|
||||
voluptuous==0.11.5 \
|
||||
noolite==1.4.0 && apt-get update -y && apt-get install -y libusb-1.0-0-dev && apt-get clean
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Define various constants."""
|
||||
# Top level-related config keys:
|
||||
CONF_ENTITY_IDS = "entity_ids"
|
||||
CONF_PROPERTIES = "properties"
|
||||
|
||||
# Entity-related config keys:
|
||||
CONF_DEVICE_TRACKERS = "device_trackers"
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ICON = "icon"
|
||||
|
||||
# Name-related config keys:
|
||||
CONF_FRIENDLY_NAME = "friendly_name"
|
||||
|
||||
# State-related config keys:
|
||||
CONF_STATE = "state"
|
||||
|
||||
# Time-related config keys:
|
||||
CONF_DELAY = "delay"
|
||||
CONF_DURATION = "duration"
|
||||
CONF_END_TIME = "end_time"
|
||||
CONF_START_TIME = "start_time"
|
||||
CONF_UPDATE_INTERVAL = "update_interval"
|
||||
|
||||
# Constraint-related config keys:
|
||||
CONF_CONSTRAIN_CLOUDY = "constrain_cloudy"
|
||||
CONF_CONSTRAIN_SUN = "constrain_sun"
|
||||
|
||||
# Notification-related config keys:
|
||||
CONF_NOTIFIERS = "notifiers"
|
||||
CONF_NOTIFICATION_INTERVAL = "notification_interval"
|
||||
CONF_NOTIFICATION_TARGET = "notification_target"
|
||||
|
||||
# Comparison-related config keys:
|
||||
CONF_ABOVE = "above"
|
||||
CONF_BELOW = "below"
|
||||
CONF_EQUAL_TO = "equal_to"
|
||||
COMPARITORS = (CONF_ABOVE, CONF_BELOW, CONF_EQUAL_TO)
|
||||
|
||||
# Collections of valid inputs:
|
||||
OPERATOR_ALL = "all"
|
||||
OPERATOR_ANY = "any"
|
||||
OPERATORS = (OPERATOR_ALL, OPERATOR_ANY)
|
||||
TOGGLE_STATES = ("closed", "off", "on", "open")
|
||||
|
||||
# Misc. config keys:
|
||||
CONF_PEOPLE = "people"
|
||||
CONF_TRIGGER_FIRST = "trigger_on_first_only"
|
||||
|
||||
# Events:
|
||||
EVENT_ALARM_CHANGE = "ALARM_CHANGE"
|
||||
EVENT_MODE_CHANGE = "MODE_CHANGE"
|
||||
EVENT_PRESENCE_CHANGE = "PRESENCE_CHANGE"
|
||||
EVENT_PROXIMITY_CHANGE = "PROXIMITY_CHANGE"
|
||||
EVENT_VACUUM_START = "VACUUM_START"
|
||||
|
||||
# Misc.:
|
||||
THRESHOLD_CLOUDY = 70.0
|
||||
@@ -0,0 +1,325 @@
|
||||
"""Define a generic object which all apps/automations inherit from."""
|
||||
from typing import Callable, Dict, List, Union
|
||||
|
||||
import voluptuous as vol
|
||||
from appdaemon.plugins.hass.hassapi import Hass # pylint: disable=no-name-in-module
|
||||
|
||||
from const import (
|
||||
CONF_ENTITY_IDS,
|
||||
CONF_PROPERTIES,
|
||||
EVENT_MODE_CHANGE,
|
||||
OPERATOR_ALL,
|
||||
OPERATORS,
|
||||
THRESHOLD_CLOUDY,
|
||||
)
|
||||
|
||||
CONF_CLASS = "class"
|
||||
CONF_MODULE = "module"
|
||||
CONF_DEPENDENCIES = "dependencies"
|
||||
|
||||
CONF_APP = "app"
|
||||
CONF_CONSTRAINTS = "constraints"
|
||||
CONF_MODE_ALTERATIONS = "mode_alterations"
|
||||
|
||||
CONF_ENABLED_TOGGLE_ENTITY_ID = "enabled_toggle_entity_id"
|
||||
CONF_INITIAL = "initial"
|
||||
CONF_NAME = "name"
|
||||
|
||||
CONF_OPERATOR = "operator"
|
||||
|
||||
SENSOR_CLOUD_COVER = "sensor.dark_sky_cloud_coverage"
|
||||
|
||||
APP_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODULE): str,
|
||||
vol.Required(CONF_CLASS): str,
|
||||
vol.Optional(CONF_APP): str,
|
||||
vol.Optional(CONF_CONSTRAINTS): vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPERATOR, default=OPERATOR_ALL): vol.In(OPERATORS),
|
||||
vol.Required(CONF_CONSTRAINTS): dict,
|
||||
}
|
||||
),
|
||||
vol.Optional(CONF_ENABLED_TOGGLE_ENTITY_ID): str,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
class Base(Hass):
|
||||
"""Define a base app/automation object."""
|
||||
|
||||
APP_SCHEMA = APP_SCHEMA
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""Initialize."""
|
||||
try:
|
||||
self.APP_SCHEMA(self.args)
|
||||
except vol.Invalid as err:
|
||||
self.error("Invalid app schema: {0}".format(err), level="ERROR")
|
||||
return
|
||||
|
||||
# Define a holding place for HASS entity IDs:
|
||||
self.entity_ids = self.args.get(CONF_ENTITY_IDS, {})
|
||||
|
||||
# Define a holding place for any scheduler handles that the app wants to keep
|
||||
# track of:
|
||||
self.handles = {} # type: Dict[str, Callable]
|
||||
|
||||
# Define a holding place for key/value properties for this app:
|
||||
self.properties = self.args.get(CONF_PROPERTIES, {})
|
||||
|
||||
# Define a holding place for any mode alterations for this app:
|
||||
self.mode_alterations = self.args.get(CONF_MODE_ALTERATIONS, [])
|
||||
|
||||
# Take every dependecy and create a reference to it:
|
||||
for app in self.args.get(CONF_DEPENDENCIES, []):
|
||||
if not getattr(self, app, None):
|
||||
setattr(self, app, self.get_app(app))
|
||||
|
||||
# Define a reference to the "manager app" – for example, a trash-
|
||||
# related app might carry a reference to TrashManager:
|
||||
if self.args.get(CONF_APP):
|
||||
self.app = getattr(self, self.args[CONF_APP])
|
||||
|
||||
# Set the entity ID of the input boolean that will control whether
|
||||
# this app is enabled or not:
|
||||
if self.args.get(CONF_ENABLED_TOGGLE_ENTITY_ID):
|
||||
self._enabled_toggle_entity_id = self.args[CONF_ENABLED_TOGGLE_ENTITY_ID]
|
||||
else:
|
||||
self._enabled_toggle_entity_id = "input_boolean.{0}".format(self.name)
|
||||
|
||||
# Register custom constraints:
|
||||
self.register_constraint("constrain_anyone")
|
||||
self.register_constraint("constrain_cloudy")
|
||||
self.register_constraint("constrain_enabled")
|
||||
self.register_constraint("constrain_everyone")
|
||||
self.register_constraint("constrain_in_blackout")
|
||||
self.register_constraint("constrain_noone")
|
||||
self.register_constraint("constrain_sun")
|
||||
|
||||
# Run any initial configuration:
|
||||
if hasattr(self, "configure"):
|
||||
self.configure()
|
||||
|
||||
# Set up connections to the automation being disabled/enabled:
|
||||
if self._enabled_entity_exists():
|
||||
# Listen and track mode changes so that the app can respond as needed:
|
||||
self.mode_events = [] # type: List[str]
|
||||
self.listen_event(self._on_mode_change, EVENT_MODE_CHANGE)
|
||||
|
||||
# If the app has defined callbacks fror when the app is enabled or disabled,
|
||||
# attach them to listeners. Note that we utilize `_on_*` here
|
||||
# (leading underscore) – we do this so automations don't have to remember
|
||||
# callback method signatures:
|
||||
if hasattr(self, "on_disable"):
|
||||
super().listen_state(
|
||||
self._on_disable, self._enabled_toggle_entity_id, new="off"
|
||||
)
|
||||
if hasattr(self, "on_enable"):
|
||||
super().listen_state(
|
||||
self._on_enable, self._enabled_toggle_entity_id, new="on"
|
||||
)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Return whether the app is enabled."""
|
||||
if not self._enabled_entity_exists():
|
||||
return True
|
||||
return self.get_state(self._enabled_toggle_entity_id) == "on"
|
||||
|
||||
def _attach_constraints(
|
||||
self, method: Callable, callback: Callable, *args: list, **kwargs: dict
|
||||
) -> Union[str, list]:
|
||||
"""Attach the constraint mechanism to an AppDaemon listener."""
|
||||
if not self.args.get(CONF_CONSTRAINTS):
|
||||
return method(callback, *args, **kwargs)
|
||||
|
||||
constraints = self.args[CONF_CONSTRAINTS][CONF_CONSTRAINTS]
|
||||
|
||||
if self.args[CONF_CONSTRAINTS].get(CONF_OPERATOR) == OPERATOR_ALL:
|
||||
return method(callback, *args, **constraints, **kwargs)
|
||||
|
||||
handles = [] # type: List[str]
|
||||
for name, value in constraints.items():
|
||||
method(callback, *args, **{name: value}, **kwargs)
|
||||
return handles
|
||||
|
||||
def _constrain_presence(self, method: str, value: Union[str, None]) -> bool:
|
||||
"""Constrain presence in a generic fashion."""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
return getattr(self.presence_manager, method)(
|
||||
*[self.presence_manager.HomeStates[s] for s in value.split(",")]
|
||||
)
|
||||
|
||||
def _enabled_entity_exists(self) -> bool:
|
||||
"""Return True if the enabled entity exists."""
|
||||
return self.entity_exists(self._enabled_toggle_entity_id)
|
||||
|
||||
def _on_disable(
|
||||
self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict
|
||||
) -> None:
|
||||
"""Set a listener for when the automation is disabled."""
|
||||
self.on_disable()
|
||||
|
||||
def _on_enable(
|
||||
self, entity: Union[str, dict], attribute: str, old: str, new: str, kwargs: dict
|
||||
) -> None:
|
||||
"""Set a listener for when the automation is enabled."""
|
||||
self.on_enable()
|
||||
|
||||
def _on_mode_change(self, event_name: str, data: dict, kwargs: dict) -> None:
|
||||
"""Compare mode changes to registered mode alterations."""
|
||||
mode = data["name"]
|
||||
|
||||
if data["state"] == "on":
|
||||
self.mode_events.append(mode)
|
||||
elif mode in self.mode_events:
|
||||
self.mode_events.remove(mode)
|
||||
|
||||
try:
|
||||
primary = max(
|
||||
(m for m in self.mode_alterations if m["mode"] in self.mode_events),
|
||||
key=lambda m: m["priority"],
|
||||
)
|
||||
except ValueError:
|
||||
try:
|
||||
primary = next((m for m in self.mode_alterations if m["mode"] == mode))
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
if primary["action"] == "enable":
|
||||
primary["action"] = "disable"
|
||||
else:
|
||||
primary["action"] = "enable"
|
||||
|
||||
# If the primary mode alteration prescribes an action that matches the state the
|
||||
# app is already in, return:
|
||||
if (self.enabled and primary["action"] == "enable") or (
|
||||
not self.enabled and primary["action"] == "disable"
|
||||
):
|
||||
return
|
||||
|
||||
if primary["action"] == "enable":
|
||||
self.enable()
|
||||
else:
|
||||
self.disable()
|
||||
|
||||
def constrain_anyone(self, value: str) -> bool:
|
||||
"""Constrain execution to whether anyone is in a state."""
|
||||
return self._constrain_presence("anyone", value)
|
||||
|
||||
def constrain_enabled(self, value: bool) -> bool:
|
||||
"""Constrain execution to whether anyone is in a state."""
|
||||
if value:
|
||||
return self.enabled
|
||||
return False
|
||||
|
||||
def constrain_cloudy(self, value: bool) -> bool:
|
||||
"""Constrain execution based whether it's cloudy or not."""
|
||||
cloud_cover = float(self.get_state(SENSOR_CLOUD_COVER))
|
||||
if (value and cloud_cover >= THRESHOLD_CLOUDY) or (
|
||||
not value and cloud_cover < THRESHOLD_CLOUDY
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def constrain_everyone(self, value: str) -> bool:
|
||||
"""Constrain execution to whether everyone is in a state."""
|
||||
return self._constrain_presence("everyone", value)
|
||||
|
||||
def constrain_in_blackout(self, state: str) -> bool:
|
||||
"""Constrain execution based on blackout state."""
|
||||
if state is True:
|
||||
return self.get_state("input_boolean.blackout_mode") == "on"
|
||||
|
||||
return self.get_state("input_boolean.blackout_mode") == "off"
|
||||
|
||||
def constrain_noone(self, value: str) -> bool:
|
||||
"""Constrain execution to whether no one is in a state."""
|
||||
return self._constrain_presence("noone", value)
|
||||
|
||||
def constrain_sun(self, position: str) -> bool:
|
||||
"""Constrain execution to the location of the sun."""
|
||||
if (position == "up" and self.sun_up()) or (
|
||||
position == "down" and self.sun_down()
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def disable(self) -> None:
|
||||
"""Disable the app."""
|
||||
if not self.entity_exists(self._enabled_toggle_entity_id):
|
||||
return
|
||||
|
||||
self.turn_off(self._enabled_toggle_entity_id)
|
||||
|
||||
def enable(self) -> None:
|
||||
"""Enable the app."""
|
||||
if not self.entity_exists(self._enabled_toggle_entity_id):
|
||||
return
|
||||
|
||||
self.turn_on(self._enabled_toggle_entity_id)
|
||||
|
||||
def listen_ios_event(self, callback: Callable, action: str) -> None:
|
||||
"""Register a callback for an iOS event."""
|
||||
self.listen_event(
|
||||
callback,
|
||||
"ios.notification_action_fired",
|
||||
actionName=action,
|
||||
constrain_enabled=True,
|
||||
)
|
||||
|
||||
def listen_event(self, callback, event=None, auto_constraints=False, **kwargs):
|
||||
"""Wrap AppDaemon's `listen_event` with the constraint mechanism."""
|
||||
if not auto_constraints:
|
||||
return super().listen_event(callback, event, **kwargs)
|
||||
|
||||
return self._attach_constraints(super().listen_event, callback, event, **kwargs)
|
||||
|
||||
def listen_state(self, callback, entity=None, auto_constraints=False, **kwargs):
|
||||
"""Wrap AppDaemon's `listen_state` with the constraint mechanism."""
|
||||
if not auto_constraints:
|
||||
return super().listen_state(callback, entity, **kwargs)
|
||||
|
||||
return self._attach_constraints(
|
||||
super().listen_state, callback, entity, **kwargs
|
||||
)
|
||||
|
||||
def run_daily(self, callback, start, auto_constraints=False, **kwargs):
|
||||
"""Wrap AppDaemon's `run_daily` with the constraint mechanism."""
|
||||
if not auto_constraints:
|
||||
return super().run_daily(callback, start, **kwargs)
|
||||
|
||||
return self._attach_constraints(super().run_daily, callback, start, **kwargs)
|
||||
|
||||
def run_at_sunrise(self, callback, *args, auto_constraints=False, **kwargs):
|
||||
"""Wrap AppDaemon's `run_at_sunrise` with the constraint mechanism."""
|
||||
if not auto_constraints:
|
||||
return super().run_at_sunrise(callback, **kwargs)
|
||||
|
||||
return self._attach_constraints(super().run_at_sunrise, callback, **kwargs)
|
||||
|
||||
def run_at_sunset(
|
||||
self,
|
||||
callback: Callable[..., None],
|
||||
*args: list,
|
||||
auto_constraints=False,
|
||||
**kwargs: dict
|
||||
):
|
||||
"""Wrap AppDaemon's `run_at_sunset` with the constraint mechanism."""
|
||||
if not auto_constraints:
|
||||
return super().run_at_sunset(callback, **kwargs)
|
||||
|
||||
return self._attach_constraints(super().run_at_sunset, callback, **kwargs)
|
||||
|
||||
def run_every(self, callback, start, interval, auto_constraints=False, **kwargs):
|
||||
"""Wrap AppDaemon's `run_every` with the constraint mechanism."""
|
||||
if not auto_constraints:
|
||||
return super().run_every(callback, start, interval, **kwargs)
|
||||
|
||||
return self._attach_constraints(
|
||||
super().run_every, callback, start, interval, **kwargs
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
import noolite
|
||||
|
||||
class Noolite(hass.Hass):
|
||||
noolite = noolite.NooLite()
|
||||
brightness = 25
|
||||
force = False
|
||||
channel = 0
|
||||
|
||||
def initialize(self):
|
||||
self.run_at_sunrise(self.sunrise_cb)
|
||||
self.run_at_sunset(self.sunset_cb)
|
||||
self.listen_state(self.light_change, "group.mainroom_light")
|
||||
self.listen_event(self.away_mode, "away_mode")
|
||||
|
||||
def away_mode(self, event_id, event_args, kwargs):
|
||||
self.noolite_off()
|
||||
|
||||
def light_change(self, entity, attribute, old, new, kwargs):
|
||||
sun_down = self.sun_down()
|
||||
if new == "off" and (sun_down or self.force):
|
||||
self.noolite_on(self.brightness)
|
||||
else:
|
||||
self.noolite_off()
|
||||
|
||||
def noolite_off(self):
|
||||
self.log("Turning off salt lamp.")
|
||||
self.noolite.off(self.channel)
|
||||
|
||||
def noolite_on(self, brightness=120):
|
||||
self.log("Turning on salt lamp for brightness {}.".format(brightness))
|
||||
self.noolite.set(self.channel, brightness)
|
||||
|
||||
def sunset_cb(self, kwargs):
|
||||
self.noolite_on(self.brightness)
|
||||
|
||||
def sunrise_cb(self, kwargs):
|
||||
self.noolite_off()
|
||||
@@ -0,0 +1,3 @@
|
||||
salt_lamp:
|
||||
module: salt_lamp
|
||||
class: Noolite
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Define the ability to send single and repeating notifications."""
|
||||
# pylint: disable=too-few-public-methods
|
||||
from datetime import datetime
|
||||
from typing import Callable, List, Optional, Union
|
||||
from uuid import uuid4
|
||||
|
||||
import attr
|
||||
|
||||
from core import Base # pylint: disable=no-name-in-module
|
||||
from notification.target import Target, get_targets_from_string
|
||||
|
||||
CONF_NOTIFICATION_HANDLES = "notification_handles"
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
class Notification:
|
||||
"""Define a base notification object."""
|
||||
|
||||
# App reference:
|
||||
_app: Base
|
||||
|
||||
# Raw targets, message, and optional title:
|
||||
targets: Union[str, List[str]]
|
||||
message: str
|
||||
title: Optional[str] = None
|
||||
|
||||
# Scheduling properties:
|
||||
urgent: bool = False
|
||||
repeat: bool = False
|
||||
when: Optional[datetime] = None
|
||||
interval: Optional[int] = None
|
||||
|
||||
iterations: Optional[int] = None
|
||||
_iteration_count: int = 0
|
||||
|
||||
# Message data
|
||||
data: Optional[dict] = None
|
||||
|
||||
# "Auto-generated" properties:
|
||||
id: str = attr.Factory(lambda: uuid4().hex)
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
"""Perform some post-__init__ initialization."""
|
||||
# Initialize a shared data space for handlers:
|
||||
if CONF_NOTIFICATION_HANDLES not in self._app.global_vars:
|
||||
self._app.global_vars[CONF_NOTIFICATION_HANDLES] = {}
|
||||
|
||||
# Give every notification a parameter that will allow it to be
|
||||
# threaded on iOS; this shouldn't hurt any non-iOS notifier:
|
||||
if not self.data:
|
||||
self.data = {}
|
||||
self.data.setdefault("push", {"thread-id": self.id})
|
||||
|
||||
def _cancel(self) -> None:
|
||||
"""Cancel the notification."""
|
||||
if self.id not in self._app.global_vars[CONF_NOTIFICATION_HANDLES]:
|
||||
return
|
||||
|
||||
handle = self._app.global_vars[CONF_NOTIFICATION_HANDLES].pop(self.id)
|
||||
if handle:
|
||||
self._app.cancel_timer(handle)
|
||||
|
||||
def _on_send(self, kwargs: dict) -> None:
|
||||
"""Send a single (immediate or scheduled) notification."""
|
||||
# If this is a repeating notification, it's already been sent once, and
|
||||
# we've exceeded our iterations, cancel right away:
|
||||
if self.iterations and self._iteration_count == self.iterations:
|
||||
self._cancel()
|
||||
return
|
||||
|
||||
if isinstance(self.targets, str):
|
||||
self.targets = [self.targets]
|
||||
|
||||
targets = [] # type: List[Target]
|
||||
for target_str in self.targets:
|
||||
targets += get_targets_from_string(self._app, target_str)
|
||||
|
||||
for target in targets:
|
||||
target.payload["data"] = self.data
|
||||
if self.title:
|
||||
target.payload["title"] = self.title
|
||||
if target.payload.get("message"):
|
||||
target.payload["message"] += self.message
|
||||
else:
|
||||
target.payload["message"] = self.message
|
||||
|
||||
self._app.call_service(target.service_call, **target.payload)
|
||||
|
||||
if self.iterations:
|
||||
self._iteration_count += 1
|
||||
|
||||
def send(self) -> Callable:
|
||||
"""Send the notification."""
|
||||
handle = None
|
||||
if self.when and self.interval:
|
||||
handle = self._app.run_every(self._on_send, self.when, self.interval)
|
||||
elif self.when:
|
||||
handle = self._app.run_at(self._on_send, self.when)
|
||||
else:
|
||||
self._on_send({})
|
||||
|
||||
self._app.global_vars[CONF_NOTIFICATION_HANDLES][self.id] = handle
|
||||
return self._cancel
|
||||
|
||||
|
||||
def send_notification(
|
||||
app: Base,
|
||||
targets: Union[str, List[str]],
|
||||
message: str,
|
||||
title: str = None,
|
||||
urgent: bool = False,
|
||||
when: datetime = None,
|
||||
interval: int = None,
|
||||
iterations: int = None,
|
||||
data: dict = None,
|
||||
) -> Callable:
|
||||
"""Send/schedule a notification and return a method to cancel it."""
|
||||
notification = Notification(
|
||||
app=app,
|
||||
targets=targets,
|
||||
message=message,
|
||||
title=title,
|
||||
urgent=urgent,
|
||||
when=when,
|
||||
interval=interval,
|
||||
iterations=iterations,
|
||||
data=data,
|
||||
)
|
||||
return notification.send()
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Define various notification target types."""
|
||||
# pylint: disable=too-few-public-methods
|
||||
from typing import Dict, List
|
||||
|
||||
from const import CONF_PEOPLE
|
||||
from core import Base # pylint: disable=no-name-in-module
|
||||
|
||||
|
||||
class Target:
|
||||
"""Define a target for generic notifications."""
|
||||
|
||||
def __init__(self, notify_service: str) -> None:
|
||||
"""Initialize."""
|
||||
self.payload = {} # type: Dict[str, str]
|
||||
self.service_call = "notify/{0}".format(notify_service)
|
||||
|
||||
|
||||
class SlackTarget(Target):
|
||||
"""Define a target for Slack notifications."""
|
||||
|
||||
def __init__(self, channel: str = None, mention: str = None) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__("slack")
|
||||
|
||||
if channel:
|
||||
self.payload["target"] = "#{0}".format(channel)
|
||||
if mention:
|
||||
self.payload["message"] = "{0}: ".format(mention)
|
||||
|
||||
|
||||
class TargetFactory:
|
||||
"""Define an abstract factory."""
|
||||
|
||||
def __init__(self, app: Base, target: str) -> None:
|
||||
"""Initialization."""
|
||||
self._app = app
|
||||
self._target = target
|
||||
|
||||
def build(self) -> List:
|
||||
"""Build notification target objects from a string representation."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class NotifierFactory(TargetFactory):
|
||||
"""Define a factory to build generic notification targets."""
|
||||
|
||||
def build(self) -> List[Target]:
|
||||
"""Build notification target objects from a string representation."""
|
||||
return [Target(self._target)]
|
||||
|
||||
|
||||
class PersonFactory(TargetFactory):
|
||||
"""Define a factory to build notification targets specific to a person."""
|
||||
|
||||
def build(self) -> List[Target]:
|
||||
"""Build notification target objects from a string representation."""
|
||||
name = self._target.split("person:")[1]
|
||||
|
||||
try:
|
||||
person = next(
|
||||
(p for p in self._app.global_vars[CONF_PEOPLE] if p.first_name == name)
|
||||
)
|
||||
except StopIteration:
|
||||
self._app.error("Unknown person: {0}".format(self._target))
|
||||
return []
|
||||
|
||||
targets = [] # type: List[Target]
|
||||
for target in person.notifiers:
|
||||
targets += get_targets_from_string(self._app, target)
|
||||
|
||||
return targets
|
||||
|
||||
|
||||
class PresenceFactory(TargetFactory):
|
||||
"""Define a factory to build notification targets specific to presence."""
|
||||
|
||||
def build(self) -> List[Target]:
|
||||
"""Build notification target objects from a string representation."""
|
||||
presence = self._target.split(":")[1]
|
||||
presence_manager = self._app.get_app("presence_manager")
|
||||
|
||||
try:
|
||||
presence_method = getattr(presence_manager, "whos_{0}".format(presence))
|
||||
except AttributeError:
|
||||
self._app.error("Unknown presence target: {0}".format(presence))
|
||||
return []
|
||||
|
||||
targets = [] # type: List[Target]
|
||||
for person in presence_method():
|
||||
for target in person.notifiers:
|
||||
targets += get_targets_from_string(self._app, target)
|
||||
|
||||
return targets
|
||||
|
||||
|
||||
class SlackFactory(TargetFactory):
|
||||
"""Define a factory to build Slack notification targets."""
|
||||
|
||||
def build(self) -> List[SlackTarget]:
|
||||
"""Build notification target objects from a string representation."""
|
||||
data = self._target.split(":")
|
||||
|
||||
if len(data) == 1:
|
||||
return [SlackTarget(None, None)]
|
||||
|
||||
splits = data[1].split("/")
|
||||
|
||||
if len(splits) == 2:
|
||||
return [SlackTarget(splits[0], splits[1])]
|
||||
|
||||
if "@" in splits[0]:
|
||||
return [SlackTarget(None, splits[0])]
|
||||
|
||||
return [SlackTarget(splits[0], None)]
|
||||
|
||||
|
||||
def get_targets_from_string(app: Base, target: str) -> List[Target]:
|
||||
"""Return the appropriate factory for the passed target."""
|
||||
if target.startswith("person:"):
|
||||
factory = PersonFactory(app, target)
|
||||
elif target.startswith("presence:"):
|
||||
factory = PresenceFactory(app, target) # type: ignore
|
||||
elif target.startswith("slack:"):
|
||||
factory = SlackFactory(app, target) # type: ignore
|
||||
else:
|
||||
factory = NotifierFactory(app, target) # type: ignore
|
||||
|
||||
return factory.build()
|
||||
@@ -36,6 +36,12 @@ class AliceScenes(hass.Hass):
|
||||
self.turn_off_light()
|
||||
if (entity_name == "night_mode"):
|
||||
self.night_mode()
|
||||
if (entity_name == "turn_on_coffeemachine"):
|
||||
self.turn_on_coffeemachine()
|
||||
if (entity_name == "turn_on_coffeemachine"):
|
||||
self.turn_on_coffeemachine()
|
||||
if (entity_name == "make_coffee"):
|
||||
self.make_coffee()
|
||||
|
||||
def terminate(self):
|
||||
if self.listen_event_handle_list != None:
|
||||
@@ -48,4 +54,17 @@ class AliceScenes(hass.Hass):
|
||||
def night_mode(self):
|
||||
self.turn_off('switch.plug_158d0001104a0c') #tv
|
||||
self.turn_off('group.all_lights')
|
||||
self.call_service("cover/close_cover", entity_id = 'cover.curtain_158d0002b0c46a')
|
||||
self.call_service("cover/close_cover", entity_id = 'cover.curtain_158d0002b0c46a')
|
||||
|
||||
def turn_on_coffeemachine(self):
|
||||
self.log('Turn on coffeemachine')
|
||||
self.call_service("mqtt/publish", topic = 'home/coffee/coffee/power/set', payload = 'on')
|
||||
|
||||
def make_coffee(self):
|
||||
self.log('Make coffee')
|
||||
self.call_service("mqtt/publish", topic = 'home/coffee/coffee/power/set', payload = 'on')
|
||||
self.timer = self.run_in(self.run_in_make_coffee, 30)
|
||||
self.log('Waiting for power on.')
|
||||
|
||||
def run_in_make_coffee(self, args):
|
||||
self.call_service("mqtt/publish", topic = 'home/coffee/coffee/coffee/set', payload = 'on')
|
||||
|
||||
@@ -13,14 +13,16 @@ import globals
|
||||
# Initial Version
|
||||
|
||||
class CurtainSecurity(hass.Hass):
|
||||
listen_event_handle_list = []
|
||||
def initialize(self):
|
||||
if not globals.check_args(self, ['curtain', 'door']):
|
||||
return
|
||||
self.listen_event(self.curtain_changed, event='call_service', entity_id = self.args['curtain'])
|
||||
# self.listen_event_handle_list = []
|
||||
# locks = globals.get_group_entities(self, "group.all_locks")
|
||||
# for lock in locks:
|
||||
# self.listen_event_handle_list.append(self.listen_state(self.lock_state_changed, lock))
|
||||
self.listen_event_handle_list.append(self.listen_event(self.curtain_changed, event='call_service', entity_id = self.args['curtain']))
|
||||
|
||||
def terminate(self):
|
||||
if self.listen_event_handle_list != None:
|
||||
for listen_event_handle in self.listen_event_handle_list:
|
||||
self.cancel_listen_event(listen_event_handle)
|
||||
|
||||
def curtain_changed(self, event_name, data, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
|
||||
@@ -36,6 +36,14 @@ class SecuritySensorsReport(hass.Hass):
|
||||
alarm_panel_state = self.get_state(self.args['ha_panel'])
|
||||
if alarm_panel_state != "armed_away":
|
||||
return
|
||||
|
||||
vacuum_state = "docked"
|
||||
if "vacuum" in self.args:
|
||||
vacuum_state = self.get_state(self.args["vacuum"])
|
||||
#Если работает пылесос и сработал датчик движения
|
||||
if vacuum_state == 'cleaning' and 'binary_sensor.motion' in entity:
|
||||
self.log('Motion sensor is triggered, but vacuum is cleaning.')
|
||||
return
|
||||
self.alarm_triggered(entity, old, new)
|
||||
|
||||
def alarm_triggered(self, entity, old, new):
|
||||
|
||||
@@ -4,6 +4,7 @@ security_sensors:
|
||||
sensor: group.security_sensors
|
||||
constraint: input_boolean.security_sensors_report
|
||||
ha_panel: alarm_control_panel.ha_alarm
|
||||
vacuum: vacuum.xiaomi_vacuum_cleaner
|
||||
notify: telegram
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -58,8 +58,8 @@ class MosEnergoSbytUpdater(Automation):
|
||||
self.set_state(entity_id, state = balance, attributes = attributes)
|
||||
|
||||
class MosEnergoSbytCounter(Base):
|
||||
_version = 11
|
||||
_device = "ios/12.1.2/2.14.14(221)"
|
||||
_version = 13
|
||||
_device = "android/11.2.6/2.9.15(105)"
|
||||
_session = None
|
||||
_profile = None
|
||||
|
||||
@@ -132,6 +132,8 @@ class MosEnergoSbytCounter(Base):
|
||||
url = "https://lkkbyt.mosenergosbyt.ru/gate/do?api_version={}&id_device={}&phone={}&ls={}&process=login&pw_abn={}&type=1".format(self._version, self._device, phone, ls, pass_md5)
|
||||
response = requests.get(url).text
|
||||
session_response = self._parse_response(response)
|
||||
if ('ID_SESSION' not in session_response):
|
||||
return
|
||||
session = session_response['ID_SESSION']
|
||||
return session
|
||||
|
||||
@@ -144,9 +146,19 @@ class MosEnergoSbytCounter(Base):
|
||||
headers.append(node.text)
|
||||
for node in et.findall('./b/p'):
|
||||
values.append(node.text)
|
||||
|
||||
result = {}
|
||||
if len(headers) == 0:
|
||||
return result
|
||||
|
||||
for i in range(0, len(headers)):
|
||||
if i>=len(headers) or i>=len(values):
|
||||
continue
|
||||
header = headers[i]
|
||||
value = values[i]
|
||||
result[header] = value
|
||||
|
||||
if 'NM_RESULT' in result and 'Вышла новая версия приложения' in result['NM_RESULT']:
|
||||
self.error('[!] new version is available. Please update: {}.'.format(result))
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
mosenergo_counter:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytCounter
|
||||
properties:
|
||||
login: !secret mosenergo_main_login
|
||||
pwd: !secret mosenergo_main_pwd
|
||||
# mosenergo_counter:
|
||||
# module: mosenergosbyt
|
||||
# class: MosEnergoSbytCounter
|
||||
# properties:
|
||||
# login: !secret mosenergo_main_login
|
||||
# pwd: !secret mosenergo_main_pwd
|
||||
|
||||
mosenergo_counter_updater:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytUpdater
|
||||
entity_ids:
|
||||
mosenergosbyt_total: sensor.power_mosenergosbyt_total
|
||||
mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
|
||||
app: mosenergo_counter
|
||||
dependencies:
|
||||
- mosenergo_counter
|
||||
global_dependencies:
|
||||
- globals
|
||||
# mosenergo_counter_updater:
|
||||
# module: mosenergosbyt
|
||||
# class: MosEnergoSbytUpdater
|
||||
# entity_ids:
|
||||
# mosenergosbyt_total: sensor.power_mosenergosbyt_total
|
||||
# mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
|
||||
# app: mosenergo_counter
|
||||
# dependencies:
|
||||
# - mosenergo_counter
|
||||
# global_dependencies:
|
||||
# - globals
|
||||
|
||||
mosenergo_counter_sender:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytSender
|
||||
entity_ids:
|
||||
home_power_total: sensor.power_main_total
|
||||
app: mosenergo_counter
|
||||
dependencies:
|
||||
- mosenergo_counter
|
||||
global_dependencies:
|
||||
- globals
|
||||
# mosenergo_counter_sender:
|
||||
# module: mosenergosbyt
|
||||
# class: MosEnergoSbytSender
|
||||
# entity_ids:
|
||||
# home_power_total: sensor.power_main_total
|
||||
# app: mosenergo_counter
|
||||
# dependencies:
|
||||
# - mosenergo_counter
|
||||
# global_dependencies:
|
||||
# - globals
|
||||
|
||||
|
||||
mosenergo_alt:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytCounter
|
||||
properties:
|
||||
login: !secret mosenergo_alt_login
|
||||
pwd: !secret mosenergo_alt_pwd
|
||||
# mosenergo_alt:
|
||||
# module: mosenergosbyt
|
||||
# class: MosEnergoSbytCounter
|
||||
# properties:
|
||||
# login: !secret mosenergo_alt_login
|
||||
# pwd: !secret mosenergo_alt_pwd
|
||||
|
||||
mosenergo_alt_updater:
|
||||
module: mosenergosbyt
|
||||
class: MosEnergoSbytUpdater
|
||||
app: mosenergo_alt
|
||||
dependencies:
|
||||
- automation
|
||||
- mosenergo_alt
|
||||
global_dependencies:
|
||||
- globals
|
||||
# mosenergo_alt_updater:
|
||||
# module: mosenergosbyt
|
||||
# class: MosEnergoSbytUpdater
|
||||
# app: mosenergo_alt
|
||||
# dependencies:
|
||||
# - automation
|
||||
# - mosenergo_alt
|
||||
# global_dependencies:
|
||||
# - globals
|
||||
@@ -12,6 +12,8 @@ class WaterCounterSender(Automation):
|
||||
self.run_daily(self.send_mosru_counters, dt.time(12, 00, 0))
|
||||
|
||||
def send_mosru_counters(self, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
self.log('Starting sending counters value to mos.ru')
|
||||
new_values = []
|
||||
self.app.update_watercounters()
|
||||
@@ -47,11 +49,38 @@ class WaterCounterSender(Automation):
|
||||
self.log('error sending: {}'.format(err))
|
||||
|
||||
|
||||
class PowerCounterSender(Automation):
|
||||
timezone = '+03:00'
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
if 'power_total' not in self.entity_ids:
|
||||
self.log('Please specify power_total for PowerCounterSender.')
|
||||
return
|
||||
self.run_daily(self.send_mosru_counters, dt.time(14, 00, 0))
|
||||
# self.send_mosru_counters("")
|
||||
|
||||
def send_mosru_counters(self, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
today = datetime.today()
|
||||
if today.day < 15 or today.day > 26:
|
||||
self.log("We can send power values between days 15 and 26.")
|
||||
return
|
||||
power_total = round(float(self.get_state(self.entity_ids['power_total'])))
|
||||
period = datetime.now().strftime('%Y-%m-%d')
|
||||
self.log('Starting sending power counters value ({}) to mosru.'.format(power_total))
|
||||
new_values = [{'counter_id': 'T1', 'indication': power_total, 'period': period}]
|
||||
try:
|
||||
result = self.app.send_electrocounters(new_values)
|
||||
self.log(result)
|
||||
except EmpServerException as err:
|
||||
self.log('error sending: {}'.format(err))
|
||||
|
||||
class WaterCounterSensor(Automation):
|
||||
timezone = '+03:00'
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||
self.timer = self.run_every(self.update_sensors, self.datetime()+timedelta(seconds=10), 1*60*60)
|
||||
|
||||
def update_sensors(self, args) -> None:
|
||||
self.log('Updating water counters')
|
||||
@@ -102,6 +131,33 @@ class WaterCounterSensor(Automation):
|
||||
|
||||
self.set_state(entity_id, state = indication_max, attributes = attributes)
|
||||
|
||||
class PowerCounterSensor(Automation):
|
||||
timezone = '+03:00'
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
self.timer = self.run_every(self.update_sensors, self.datetime()+timedelta(seconds=30), 1*60*60)
|
||||
|
||||
def update_sensors(self, args) -> None:
|
||||
self.log('Updating power counters')
|
||||
self.app.update_power()
|
||||
if 'power_total' in self.entity_ids:
|
||||
power_total = self.app.power_total
|
||||
self.create_power_entity(self.entity_ids['power_total'], power_total, 'kWh', 'Счетчик')
|
||||
if 'power_balance' in self.entity_ids:
|
||||
power_balance = self.app.power_balance
|
||||
self.create_power_entity(self.entity_ids['power_balance'], power_balance, '₽', 'Баланс')
|
||||
|
||||
def create_power_entity(self, entity_id, value, unit, description):
|
||||
icon = "mdi:av-timer"
|
||||
attributes = {}
|
||||
attributes['unit_of_measurement'] = unit
|
||||
attributes['icon'] = icon
|
||||
attributes['friendly_name'] = "{}(mos.ru)".format(description)
|
||||
attributes['description'] = 'Created and updated from appdaemon ({})'.format(__name__)
|
||||
|
||||
self.set_state(entity_id, state = value, attributes = attributes)
|
||||
|
||||
|
||||
class EpdBalanceSensor(Automation):
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
@@ -184,6 +240,11 @@ class MosruPower(MosruBase):
|
||||
self._check_login()
|
||||
self._power_counters = self._api.get_electrocounters(self._flat_id)
|
||||
|
||||
def send_electrocounters(self, new_values) -> None:
|
||||
self._check_login()
|
||||
self._api.send_electrocounters(self._flat_id, new_values)
|
||||
|
||||
|
||||
class MosruWater(MosruBase):
|
||||
_water_counters = None
|
||||
|
||||
|
||||
@@ -25,6 +25,19 @@ epd_sensor:
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
|
||||
power_sensor:
|
||||
class: PowerCounterSensor
|
||||
module: mosru
|
||||
entity_ids:
|
||||
power_balance: sensor.power_mosenergosbyt_balance
|
||||
power_total: sensor.power_mosenergosbyt_total
|
||||
app: mosru_client
|
||||
dependencies:
|
||||
- mosru_client
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
water_sensor:
|
||||
class: WaterCounterSensor
|
||||
module: mosru
|
||||
@@ -49,3 +62,14 @@ water_sender:
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
power_sender:
|
||||
class: PowerCounterSender
|
||||
module: mosru
|
||||
entity_ids:
|
||||
power_total: sensor.power_main_total
|
||||
app: mosru_client
|
||||
dependencies:
|
||||
- mosru_client
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
from notification import send_notification
|
||||
import datetime
|
||||
# from automation import Automation # type: ignore
|
||||
|
||||
"""
|
||||
Monitor events and output changes to the verbose_log. Nice for debugging purposes.
|
||||
Arguments:
|
||||
- events: List of events to monitor
|
||||
"""
|
||||
class Test(hass.Hass):
|
||||
def initialize(self) -> None:
|
||||
self.log('test')
|
||||
# id = send_notification(
|
||||
# self, targets="telegram_monster", message="Repeated message", title="title", when=datetime.datetime.now(), interval=10, iterations=2
|
||||
# )
|
||||
# self.log("id:{}".format(id))
|
||||
# id = send_notification(
|
||||
# self, targets="telegram_monster", message="Dummy", title="title"
|
||||
# )
|
||||
|
||||
def terminate(self):
|
||||
self.log('terminate')
|
||||
# for listen_event_handle in self.listen_event_handle_list:
|
||||
# self.cancel_listen_event(listen_event_handle)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
test:
|
||||
module: test
|
||||
class: Test
|
||||
@@ -7,14 +7,14 @@ led_timer:
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
led_window_timer:
|
||||
module: entity_timer
|
||||
class: EntityTimer
|
||||
on_time: "09:00:00"
|
||||
off_time: "23:00:00"
|
||||
entity: switch.wifi_socket
|
||||
global_dependencies:
|
||||
- globals
|
||||
# led_window_timer:
|
||||
# module: entity_timer
|
||||
# class: EntityTimer
|
||||
# on_time: "09:00:00"
|
||||
# off_time: "23:00:00"
|
||||
# entity: switch.wifi_socket
|
||||
# global_dependencies:
|
||||
# - globals
|
||||
|
||||
xmas_tree_timer:
|
||||
module: entity_timer
|
||||
|
||||
+4
-1
@@ -46,10 +46,13 @@ services:
|
||||
build:
|
||||
context: ./appdaemon/docker
|
||||
args:
|
||||
VERSION: 3.0.2
|
||||
VERSION: 3.0.5
|
||||
container_name: appdaemon
|
||||
depends_on:
|
||||
- hass
|
||||
privileged: true
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
volumes:
|
||||
- ./appdaemon/settings:/conf
|
||||
- ./hass/settings/secrets.yaml:/conf/secrets.yaml:ro
|
||||
|
||||
@@ -9,6 +9,8 @@ filter:
|
||||
include_entities:
|
||||
- input_boolean.alice_night_mode
|
||||
- input_boolean.alice_turn_off_light
|
||||
- input_boolean.alice_turn_on_coffeemachine
|
||||
- input_boolean.alice_make_coffee
|
||||
|
||||
exclude_entities:
|
||||
- switch.plug_158d00020f3a29
|
||||
|
||||
@@ -6,3 +6,10 @@ alice_turn_off_light:
|
||||
name: Выключи свет
|
||||
initial: off
|
||||
|
||||
alice_turn_on_coffeemachine:
|
||||
name: Включи кофемашину
|
||||
initial: off
|
||||
|
||||
alice_make_coffee:
|
||||
name: Сделай кофе
|
||||
initial: off
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# - platform: noolite
|
||||
# channel: 0
|
||||
# name: "salt_lamp"
|
||||
# scan_interval: 60
|
||||
# type: dimmer
|
||||
# mode: noolite
|
||||
@@ -1,10 +1,10 @@
|
||||
# water_counter_set:
|
||||
# sequence:
|
||||
# - service: mqtt.publish
|
||||
# data:
|
||||
# topic: 'home/bath/bath/cold/set'
|
||||
# payload: 127110
|
||||
# - service: mqtt.publish
|
||||
# data:
|
||||
# topic: 'home/bath/bath/hot/set'
|
||||
# payload: 67270
|
||||
water_counter_set:
|
||||
sequence:
|
||||
- service: mqtt.publish
|
||||
data:
|
||||
topic: 'home/bath/bath/cold/set'
|
||||
payload: 199820
|
||||
- service: mqtt.publish
|
||||
data:
|
||||
topic: 'home/bath/bath/hot/set'
|
||||
payload: 104171
|
||||
|
||||
Submodule hass/settings/custom_components/yandex_smart_home updated: d9ef78dfc5...faa54aed7f
Reference in New Issue
Block a user