Compare commits

...

10 Commits

Author SHA1 Message Date
John Lancaster
4ff1ac573f removed docker observation 2024-12-01 23:25:02 -06:00
John Lancaster
6fd891b0da added log line 2024-10-14 00:48:35 +00:00
John Lancaster
28f2c4d094 fixed kwargs 2024-10-14 00:48:24 +00:00
John Lancaster
ea5a9542e1 TV sleep timer app 2024-10-14 00:32:46 +00:00
John Lancaster
3781a36be6 tweaks 2024-10-13 03:04:04 +00:00
John Lancaster
92b100e0ce added critical notification example 2024-10-13 02:47:26 +00:00
John Lancaster
2c89a044d4 started traffic notification app 2024-10-04 13:14:54 +00:00
John Lancaster
96e22ac46d changed host IP to make reverse proxy work 2024-08-30 17:14:53 -05:00
John Lancaster
e933e1c2b5 moved weather example 2024-08-30 16:56:35 -05:00
John Lancaster
72b91c1c76 submodule bump 2024-08-30 16:55:35 -05:00
14 changed files with 183 additions and 69 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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

View File

@@ -0,0 +1,5 @@
weather:
module: weather
class: Weather
# log_level: DEBUG
location: 78704 US

View File

@@ -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()

View File

@@ -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',

View File

@@ -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
View 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

View File

@@ -0,0 +1,3 @@
traffic:
module: traffic
class: TrafficAlert