Compare commits
10 Commits
6970bab14e
...
4ff1ac573f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ff1ac573f | ||
|
|
6fd891b0da | ||
|
|
28f2c4d094 | ||
|
|
ea5a9542e1 | ||
|
|
3781a36be6 | ||
|
|
92b100e0ce | ||
|
|
2c89a044d4 | ||
|
|
96e22ac46d | ||
|
|
e933e1c2b5 | ||
|
|
72b91c1c76 |
@@ -1,10 +0,0 @@
|
|||||||
FROM python:3.10
|
|
||||||
|
|
||||||
# install order matters because of some weird dependency stuff with websocket-client
|
|
||||||
# install appdaemon first because it's versioning is more restrictive
|
|
||||||
RUN pip install git+https://github.com/AppDaemon/appdaemon@dev
|
|
||||||
|
|
||||||
ENV CONF=/conf
|
|
||||||
RUN mkdir $CONF
|
|
||||||
COPY ./requirements.txt ${CONF}
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip pip install -r ${CONF}/requirements.txt
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
|
||||||
{
|
|
||||||
"name": "Appdaemon",
|
|
||||||
"build": {
|
|
||||||
// Sets the run context to one level up instead of the .devcontainer folder.
|
|
||||||
"context": "..",
|
|
||||||
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
|
|
||||||
"dockerfile": "../.devcontainer/Dockerfile"
|
|
||||||
},
|
|
||||||
|
|
||||||
"mounts": [
|
|
||||||
"source=/etc/localtime,target=/etc/localtime,type=bind,consistency=cached",
|
|
||||||
"source=/etc/timezone,target=/etc/timezone,type=bind,consistency=cached",
|
|
||||||
"source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind,consistency=cached"
|
|
||||||
],
|
|
||||||
|
|
||||||
"workspaceMount": "source=${localWorkspaceFolder},target=/conf,type=bind",
|
|
||||||
"workspaceFolder": "/conf",
|
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/git:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
|
|
||||||
// Uncomment the next line to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "cat /etc/os-release",
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
|
||||||
"extensions": [
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-toolsai.jupyter",
|
|
||||||
"mhutchie.git-graph",
|
|
||||||
"ms-python.isort",
|
|
||||||
"ms-python.autopep8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "devcontainer"
|
|
||||||
}
|
|
||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -2,7 +2,3 @@
|
|||||||
path = apps/room_control
|
path = apps/room_control
|
||||||
url = ssh://gitea/john/room_control
|
url = ssh://gitea/john/room_control
|
||||||
branch = main
|
branch = main
|
||||||
[submodule "docker-observation"]
|
|
||||||
path = docker-observation
|
|
||||||
url = ssh://gitea/john/docker-observation
|
|
||||||
branch = main
|
|
||||||
@@ -31,7 +31,7 @@ appdaemon:
|
|||||||
admin:
|
admin:
|
||||||
api:
|
api:
|
||||||
http:
|
http:
|
||||||
url: http://127.0.0.1:5050
|
url: http://0.0.0.0:5050
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
main_log:
|
main_log:
|
||||||
|
|||||||
68
apps/examples/critical.py
Normal file
68
apps/examples/critical.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from appdaemon import Hass
|
||||||
|
from appdaemon.entity import Entity
|
||||||
|
from appdaemon.plugins.hass.notifications import AndroidNotification
|
||||||
|
|
||||||
|
|
||||||
|
class CriticalAlert(Hass):
|
||||||
|
msg: str
|
||||||
|
alert_active: bool = False
|
||||||
|
alert_handle: str = None
|
||||||
|
context: str = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.set_log_level('DEBUG')
|
||||||
|
self.critical_sensor.listen_state(self.alert)
|
||||||
|
self.listen_notification_action(self.clear_alert, 'clear')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def msg(self) -> str:
|
||||||
|
return self.args['msg']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self) -> str:
|
||||||
|
return self.args['device']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def critical_sensor(self) -> Entity:
|
||||||
|
return self.get_entity(self.args['sensor'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def critical_notification(self) -> AndroidNotification:
|
||||||
|
n = AndroidNotification(
|
||||||
|
device=self.args['device'],
|
||||||
|
message=self.msg,
|
||||||
|
tag=self.name,
|
||||||
|
)
|
||||||
|
n.color = 'red'
|
||||||
|
n.icon = 'fire-alert'
|
||||||
|
n.add_action('clear', 'Clear Alert')
|
||||||
|
return n
|
||||||
|
|
||||||
|
def alert(self, entity: str, attribute: str, old: str, new: str, **kwargs):
|
||||||
|
self.alert_active = new == 'on'
|
||||||
|
|
||||||
|
if self.alert_active:
|
||||||
|
self.alert_handle = self.run_every(
|
||||||
|
self.repeat_alert,
|
||||||
|
start='now', interval=2.0
|
||||||
|
)
|
||||||
|
self.log(f'Alert Handle: {self.alert_handle}', level='DEBUG')
|
||||||
|
else:
|
||||||
|
if self.alert_handle:
|
||||||
|
self.clear_alert()
|
||||||
|
|
||||||
|
def repeat_alert(self, **kwargs):
|
||||||
|
if self.alert_active:
|
||||||
|
self.android_tts(
|
||||||
|
device=self.device,
|
||||||
|
tts_text=self.msg,
|
||||||
|
critical=True
|
||||||
|
)
|
||||||
|
self.call_service(**self.critical_notification.to_service_call())
|
||||||
|
|
||||||
|
def clear_alert(self, event_name: str = None, data: dict = None, **cb_args: dict):
|
||||||
|
self.log(event_name, level='DEBUG')
|
||||||
|
if self.alert_active:
|
||||||
|
self.alert_active = False
|
||||||
|
self.cancel_timer(self.alert_handle)
|
||||||
|
self.alert_handle = None
|
||||||
5
apps/examples/weather/weather.yaml
Normal file
5
apps/examples/weather/weather.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
weather:
|
||||||
|
module: weather
|
||||||
|
class: Weather
|
||||||
|
# log_level: DEBUG
|
||||||
|
location: 78704 US
|
||||||
Submodule apps/room_control updated: 2180e544c0...e3186f1b5e
@@ -18,7 +18,7 @@ class SceneDetector(Hass):
|
|||||||
)
|
)
|
||||||
self.log(f"Waiting for scene '{self.scene_entity.friendly_name}' to activate")
|
self.log(f"Waiting for scene '{self.scene_entity.friendly_name}' to activate")
|
||||||
|
|
||||||
async def event_callback(self, event_name, data, cb_args):
|
async def event_callback(self, event_name, data, **kwargs):
|
||||||
entity_id = data['service_data']['entity_id']
|
entity_id = data['service_data']['entity_id']
|
||||||
if entity_id == self.scene_entity.entity_id:
|
if entity_id == self.scene_entity.entity_id:
|
||||||
await self.scene_detected()
|
await self.scene_detected()
|
||||||
|
|||||||
@@ -1,14 +1,45 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from appdaemon.entity import Entity
|
from appdaemon.entity import Entity
|
||||||
from appdaemon.plugins.hass.hassapi import Hass
|
from appdaemon.plugins.hass.hassapi import Hass
|
||||||
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
||||||
|
from appdaemon.adbase import ADBase
|
||||||
|
|
||||||
|
|
||||||
|
class SleepTV(ADBase):
|
||||||
|
handle: str = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.adapi = self.get_ad_api()
|
||||||
|
self.adapi.set_log_level('DEBUG')
|
||||||
|
self.sleep_time.listen_state(self.handle_sleep_time_change)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sleep_time(self) -> Entity:
|
||||||
|
return self.adapi.get_entity(self.args['sleep_time'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tv(self) -> Entity:
|
||||||
|
return self.adapi.get_entity(self.args['tv'])
|
||||||
|
|
||||||
|
def handle_sleep_time_change(self, entity: str, attribute: str, old: str, new: str, **kwargs):
|
||||||
|
now: datetime = self.adapi.get_now()
|
||||||
|
dt = datetime.strptime(new, '%H:%M:%S')
|
||||||
|
dt = datetime.combine(now.date(), dt.time())
|
||||||
|
|
||||||
|
if dt.time() < now.time():
|
||||||
|
dt += timedelta(days=1)
|
||||||
|
|
||||||
|
self.adapi.cancel_timer(self.handle, silent=True)
|
||||||
|
self.handle = self.adapi.run_at(lambda **kwargs: self.tv.turn_off(), dt)
|
||||||
|
self.adapi.log(f'Turning TV off: {dt.strftime("%a %I:%M:%S %p")}')
|
||||||
|
|
||||||
|
|
||||||
class SleepSetter(Hass, Mqtt):
|
class SleepSetter(Hass, Mqtt):
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
assert self.entity_exists(entity_id=self.variable), f'{self.variable} does not exist'
|
assert self.entity_exists(entity_id=self.variable), f'{self.variable} does not exist'
|
||||||
self.listen_state(callback=self.handle_state, entity_id=self.variable)
|
self.variable_entity.listen_state(self.handle_state)
|
||||||
self.setup_buttons()
|
self.setup_buttons()
|
||||||
|
|
||||||
def setup_buttons(self):
|
def setup_buttons(self):
|
||||||
@@ -20,7 +51,7 @@ class SleepSetter(Hass, Mqtt):
|
|||||||
|
|
||||||
def setup_button(self, name: str):
|
def setup_button(self, name: str):
|
||||||
topic = f'zigbee2mqtt/{name}'
|
topic = f'zigbee2mqtt/{name}'
|
||||||
self.mqtt_subscribe(topic, namespace='mqtt')
|
# self.mqtt_subscribe(topic, namespace='mqtt')
|
||||||
self.listen_event(
|
self.listen_event(
|
||||||
self.handle_button,
|
self.handle_button,
|
||||||
'MQTT_MESSAGE',
|
'MQTT_MESSAGE',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ sleep:
|
|||||||
- Bedroom Button 2
|
- Bedroom Button 2
|
||||||
- Living Room Button
|
- Living Room Button
|
||||||
- Bathroom Button
|
- Bathroom Button
|
||||||
|
|
||||||
off_apps:
|
off_apps:
|
||||||
# - bedroom
|
# - bedroom
|
||||||
- living_room
|
- living_room
|
||||||
@@ -21,3 +22,9 @@ sleep:
|
|||||||
on_apps:
|
on_apps:
|
||||||
- living_room
|
- living_room
|
||||||
- bedroom
|
- bedroom
|
||||||
|
|
||||||
|
sleep_tv:
|
||||||
|
module: sleep
|
||||||
|
class: SleepTV
|
||||||
|
sleep_time: input_datetime.tv_sleep_time
|
||||||
|
tv: media_player.bedroom_vizio
|
||||||
|
|||||||
64
apps/traffic/traffic.py
Normal file
64
apps/traffic/traffic.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from appdaemon import ADAPI
|
||||||
|
from appdaemon.adbase import ADBase
|
||||||
|
from appdaemon.entity import Entity
|
||||||
|
from appdaemon.models.notification import AndroidData
|
||||||
|
|
||||||
|
|
||||||
|
class TrafficAlert(ADAPI):
|
||||||
|
notified: bool = False
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.log(f'Traffic time: {self.traffic_time}')
|
||||||
|
self.traffic_entity.listen_state(
|
||||||
|
self.handle_state_change,
|
||||||
|
attribute='duration',
|
||||||
|
# constrain_state=lambda d: d > 30.0
|
||||||
|
)
|
||||||
|
self.handle_state_change(new=self.traffic_time.total_seconds() / 60)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def traffic_entity(self) -> Entity:
|
||||||
|
return self.get_entity('sensor.work_to_home')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def traffic_time(self) -> timedelta:
|
||||||
|
return timedelta(minutes=float(self.traffic_entity.get_state('duration')))
|
||||||
|
|
||||||
|
def notify_android(self, device: str, **data):
|
||||||
|
model = AndroidData.model_validate(data)
|
||||||
|
res = self.call_service(
|
||||||
|
f'notify/mobile_app_{device}', **model.model_dump())
|
||||||
|
return res
|
||||||
|
|
||||||
|
def handle_state_change(self,
|
||||||
|
entity: str = None,
|
||||||
|
attribute: str = None,
|
||||||
|
old: str = None,
|
||||||
|
new: str = None,
|
||||||
|
**kwargs: dict):
|
||||||
|
self.log(f'Travel time changed: {new}')
|
||||||
|
|
||||||
|
if not self.notified:
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
'action': 'URI',
|
||||||
|
'title': 'See travel times',
|
||||||
|
'uri': "https://grafana.john-stream.com/d/f4ff212e-c786-40eb-b615-205121f482e3/travel-time-details?orgId=1&from=1727927004390&to=1728013404390"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if new > 40.0:
|
||||||
|
self.notify_android('pixel_5', **{
|
||||||
|
'title': 'Heavy Traffic',
|
||||||
|
'message': 'Get moving!',
|
||||||
|
'data': {'tag': 'traffic', 'color': 'red', 'actions': actions},
|
||||||
|
})
|
||||||
|
elif new > 30.0:
|
||||||
|
self.notify_android('pixel_5', **{
|
||||||
|
'title': 'Increaing Traffic',
|
||||||
|
'message': 'Something needs to happen',
|
||||||
|
'data': {'tag': 'traffic', 'color': 'yellow', 'actions': actions},
|
||||||
|
})
|
||||||
|
self.notified = True
|
||||||
3
apps/traffic/traffic.yaml
Normal file
3
apps/traffic/traffic.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
traffic:
|
||||||
|
module: traffic
|
||||||
|
class: TrafficAlert
|
||||||
Submodule docker-observation deleted from 1ee44b7b80
Reference in New Issue
Block a user