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
url = ssh://gitea/john/room_control
branch = main
[submodule "docker-observation"]
path = docker-observation
url = ssh://gitea/john/docker-observation
branch = main

View File

@@ -31,7 +31,7 @@ appdaemon:
admin:
api:
http:
url: http://127.0.0.1:5050
url: http://0.0.0.0:5050
logs:
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")
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']
if entity_id == self.scene_entity.entity_id:
await self.scene_detected()

View File

@@ -1,14 +1,45 @@
from datetime import datetime, timedelta
import json
from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass
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):
def initialize(self):
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()
def setup_buttons(self):
@@ -20,7 +51,7 @@ class SleepSetter(Hass, Mqtt):
def setup_button(self, name: str):
topic = f'zigbee2mqtt/{name}'
self.mqtt_subscribe(topic, namespace='mqtt')
# self.mqtt_subscribe(topic, namespace='mqtt')
self.listen_event(
self.handle_button,
'MQTT_MESSAGE',

View File

@@ -9,6 +9,7 @@ sleep:
- Bedroom Button 2
- Living Room Button
- Bathroom Button
off_apps:
# - bedroom
- living_room
@@ -21,3 +22,9 @@ sleep:
on_apps:
- living_room
- 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