Compare commits

..

184 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
John Lancaster 6970bab14e fixed patio 2024-08-30 08:35:07 -05:00
John Lancaster 9e01dc7958 submodule update 2024-08-27 20:43:14 -05:00
John Lancaster fca47b8a60 silencing warnings 2024-08-27 19:17:02 -05:00
John Lancaster 4114fb4c54 more changes for dictionary unpacking 2024-08-27 19:14:26 -05:00
John Lancaster a8e0b53c53 changes for use_dictionary_unpacking 2024-08-27 00:42:19 -05:00
John Lancaster de31b6ca5a added patio light 2024-08-13 01:07:00 -05:00
John Lancaster 3bd955299c updated scene detect app 2024-08-02 18:28:44 -05:00
John Lancaster a73153298a changes for the persistent namespace 2024-08-02 10:04:06 -05:00
John Lancaster 447f8bd354 submodule update 2024-07-31 23:26:19 -05:00
John Lancaster 689a9d0211 single apps per room again based on services update 2024-07-27 17:38:09 -05:00
John Lancaster dbdc088ca0 added update script 2024-06-15 17:15:57 -05:00
John Lancaster 4014c60148 async fix 2024-06-12 08:01:30 -05:00
John Lancaster 2355aadab7 tweaks 2024-06-11 23:21:54 -05:00
John Lancaster 8bbe64a9ec added loki handler 2024-06-11 23:04:34 -05:00
John Lancaster bc175f75d9 improved scene detect 2024-06-11 23:03:59 -05:00
John Lancaster 5b56c29999 docker observation update 2024-05-20 18:30:48 -05:00
John Lancaster 94fda2f84d submodule update 2024-05-07 23:39:25 -05:00
John Lancaster 8eb9e84b9d formatting 2024-05-07 23:37:34 -05:00
John Lancaster a38341484d expert import mode 2024-05-02 22:49:46 -05:00
John Lancaster aedee1ac12 submodule update 2024-04-29 23:42:43 -05:00
John Lancaster c4c4364225 set up dependencies and rich logging 2024-04-29 23:41:19 -05:00
John Lancaster 4bf09a677b submodule update 2024-04-02 22:48:15 -05:00
John Lancaster 38e1cdaae2 point submodule at gitea instead of github 2024-04-02 22:46:34 -05:00
John Lancaster 7ee2b14008 submodule updates 2024-04-02 22:34:10 -05:00
John Lancaster 01af9e106c tweaked types 2024-04-02 22:03:11 -05:00
John Lancaster f64605b831 tweaked white space 2024-04-02 22:02:47 -05:00
John Lancaster da7e2b2d50 removed obsolete version number 2024-04-02 22:01:10 -05:00
John Lancaster 1fc7e45d26 successful pydantic experiments 2024-03-16 11:23:53 -05:00
John Lancaster 055cf4e51c better log level selection 2024-03-10 17:36:43 -05:00
John Lancaster b6223a95d2 updated for rich logging stuff 2024-03-10 17:36:14 -05:00
John Lancaster a79d441668 cube updates 2024-03-04 19:23:07 -06:00
John Lancaster 2c77c3aa66 formatting 2024-03-03 12:34:33 -06:00
John Lancaster d7312bb3ef cleaned up 2024-03-03 11:27:03 -06:00
John Lancaster 5db2976ce6 fixed soundbar turning on 2024-03-03 10:50:09 -06:00
John Lancaster 8383ff5cf1 submodule update 2024-02-25 15:25:46 -06:00
John Lancaster 8a84031813 submodule update 2024-02-25 15:07:26 -06:00
John Lancaster 522c41e733 submodule update 2024-02-25 13:57:40 -06:00
John Lancaster 88d1badf53 added docker-observation submodule 2024-02-24 15:51:15 -06:00
John Lancaster 489076f011 added readme 2024-01-28 09:06:39 -06:00
John Lancaster 88dbd0fd3f Merge branch 'async' 2024-01-28 09:05:04 -06:00
John Lancaster af8d13bfce pre-merge (kinda fucked up before) 2024-01-28 09:04:54 -06:00
John Lancaster dee31fe6f1 removed bedroom from off_apps 2024-01-28 09:02:27 -06:00
John Lancaster 2a66ca4521 updated TV for reset 2024-01-28 09:02:06 -06:00
John Lancaster 0eb484f187 expanded callback arguments 2024-01-28 08:53:51 -06:00
John Lancaster 28d0541284 removed async 2024-01-28 08:53:05 -06:00
John Lancaster 986996c595 fixed toggle 2024-01-28 08:48:00 -06:00
John Lancaster 27c89cf542 submodule update 2024-01-28 08:47:45 -06:00
John Lancaster 4ab88d11ca had to change from color name to RGB for some reason 2024-01-21 10:57:51 -06:00
John Lancaster e71b003099 room_control update 2024-01-15 23:04:34 -06:00
John Lancaster 2cd374a927 fixed leaving? 2024-01-08 20:24:27 -06:00
John Lancaster 496664e106 added bathroom button to sleep setter 2023-12-08 08:23:20 -06:00
John Lancaster b93b308131 room_control update 2023-12-08 08:23:09 -06:00
John Lancaster e3735e381b fixed brightness_pct after update 2023-12-08 08:18:56 -06:00
John Lancaster 56e4d893f5 changed to async branch for room_control submodule 2023-12-07 00:33:20 -06:00
John Lancaster 7456174ef6 room control update 2023-12-07 00:28:45 -06:00
John Lancaster 80392fbf95 rearrange 2023-12-04 18:03:37 -06:00
John Lancaster 8d303438ce read only on config folder 2023-12-03 18:04:30 -06:00
John Lancaster 4b0b7c5e61 yaml tweaks 2023-12-03 18:04:16 -06:00
John Lancaster 0102321b87 fixed scene watchers 2023-11-25 22:36:41 -06:00
John Lancaster f7d8fba8fb gixed on apps 2023-11-25 22:36:18 -06:00
John Lancaster 07330e442d leaving update 2023-11-25 22:09:10 -06:00
John Lancaster 00a7fd5048 toggle stuff 2023-11-25 22:06:43 -06:00
John Lancaster 9d0b5e4331 room control update 2023-11-25 18:51:33 -06:00
John Lancaster 3b0b4e07bb sleep update 2023-11-25 18:51:27 -06:00
John Lancaster f877cab66d better logging 2023-11-25 18:50:57 -06:00
John Lancaster e644eefa6d added await to MotionCanceller 2023-11-25 18:50:47 -06:00
John Lancaster 021dc61c6b renames 2023-11-25 18:23:31 -06:00
John Lancaster daf97fcb06 simplified a bit 2023-11-25 18:23:19 -06:00
John Lancaster 0b96430f7a sleep tweak 2023-11-25 10:18:33 -06:00
John Lancaster 89576840a5 started async stuff 2023-11-25 09:55:38 -06:00
John Lancaster d34dfd2e56 ref entity business 2023-11-25 02:18:00 -06:00
John Lancaster 117c7b3471 late night scene 2023-11-25 01:34:44 -06:00
John Lancaster 648bbc2a5b moved ref entity 2023-11-25 01:12:33 -06:00
John Lancaster 366e048456 broke out motion 2023-11-25 00:55:13 -06:00
John Lancaster b4a6d9ac78 broke out button function 2023-11-24 23:18:42 -06:00
John Lancaster 380be7d4a6 broke out door function 2023-11-24 22:56:42 -06:00
John Lancaster 35f60f7b1a trimmed requirements 2023-11-24 22:00:06 -06:00
John Lancaster 79beb97945 added causes 2023-11-24 21:48:33 -06:00
John Lancaster 1ce2e73f77 reorg 2023-11-24 21:40:18 -06:00
John Lancaster 525b519433 mqtt improvements to sleep setter 2023-11-24 21:37:54 -06:00
John Lancaster d5e1cb174d added leaving app 2023-11-24 21:02:25 -06:00
John Lancaster 1f30147a71 some custom apps 2023-11-24 16:39:03 -06:00
John Lancaster 1fbe4d3dae incorporated other logic 2023-11-24 16:38:40 -06:00
John Lancaster 964b7fa0b0 generic action handler 2023-11-24 16:34:38 -06:00
John Lancaster f4c73c5ef7 room control update 2023-11-24 16:34:21 -06:00
John Lancaster d57032bfb5 sleep setter update 2023-11-24 16:06:50 -06:00
John Lancaster 0deb3eb8f6 generic action handler 2023-11-24 16:06:07 -06:00
John Lancaster 9597cf5942 aqara cube base 2023-11-24 15:43:44 -06:00
John Lancaster 0859a571ea generalized 2023-11-24 14:56:54 -06:00
John Lancaster 5c20718f87 tuned 2023-11-24 14:56:26 -06:00
John Lancaster 10d19e99d1 scene detector 2023-11-24 14:49:18 -06:00
John Lancaster d72ebbdeb4 mqtt buttons 2023-11-24 14:33:14 -06:00
John Lancaster 08f3f95207 doc update 2023-11-24 01:31:04 -06:00
root b66879703f changed mqtt brokers 2023-11-19 18:02:17 -06:00
root 173f89eea5 updated room control 2023-11-19 16:17:35 -06:00
root 7b859b682d enabled color logging 2023-11-01 22:37:02 -05:00
root 1be85ae225 added python version to gitignore 2023-07-30 23:44:16 -05:00
root 8917c8dbd7 submodule update 2023-07-30 23:24:01 -05:00
root 6359ac9105 added ha_button for kitchen 2023-07-30 23:01:36 -05:00
root 688df0185f added mqtt plugin 2023-07-30 23:01:21 -05:00
root 80b7c50216 config update 2023-07-30 22:47:56 -05:00
root d31298fa9f submodule update 2023-07-30 14:59:21 -05:00
root 6326c95748 added git feature to dev container 2023-07-30 14:38:04 -05:00
root 9a77854f85 key rename 2023-07-30 14:23:06 -05:00
root 82d8136e97 added room_control as submodule 2023-07-30 13:51:46 -05:00
root c119765a38 type hint 2023-07-29 21:41:19 -05:00
root bbc3e94a7e fixed dockerfile 2023-07-29 21:34:33 -05:00
root 162bd80409 more cleanup 2023-07-29 21:34:25 -05:00
root 7a08bd4f8e updated docker build script 2023-07-29 21:16:37 -05:00
root 88c28248fd cleanups 2023-07-29 18:51:22 -05:00
root c80b665f95 migrated more motion sensors 2023-07-21 08:26:39 -05:00
root 7f1083cc5d changed volume to relative path 2023-07-18 22:32:20 -05:00
John Lancaster 73d2ea117e added sleeping_active property 2023-07-17 17:36:09 -05:00
John Lancaster c621b8d31b speaker control 2023-07-17 17:35:30 -05:00
John Lancaster 61ae9f1149 migrated bathroom motion sensor 2023-07-17 17:35:17 -05:00
John Lancaster 46c3e8f951 added notebooks to the git ignore 2023-07-17 17:35:04 -05:00
John Lancaster aa3cf277e7 added globe to sleep entities 2023-07-17 17:27:05 -05:00
John Lancaster 7407a39bbb migration instructions 2023-07-17 17:26:39 -05:00
John Lancaster 243e6de04f bug fix 2023-06-26 00:42:01 -05:00
John Lancaster 2ee2155265 added globe light 2023-06-26 00:41:52 -05:00
John Lancaster 0850828bda broke out the hardware entity 2023-05-07 17:13:25 -05:00
John Lancaster a9f37c5e03 pulled out some variables 2023-05-07 17:05:50 -05:00
John Lancaster 206f943e1a initial TV control 2023-05-07 16:50:10 -05:00
John Lancaster 8880a409ff removed obsolete yamls 2023-05-06 12:24:08 -05:00
John Lancaster e4f2a1f625 moved basic_motion module 2023-05-06 12:22:55 -05:00
John Lancaster 700c8e1214 reworked callback cancellation 2023-04-30 17:08:50 -05:00
John Lancaster cd22364e21 migrated rooms.yaml 2023-04-27 23:24:09 -05:00
John Lancaster 32bc512d37 bug fix 2023-04-27 23:23:54 -05:00
John Lancaster aa5c0d0092 changed state determination 2023-04-27 23:05:31 -05:00
John Lancaster ad2eda75cc small formatting 2023-04-26 22:39:22 -05:00
John Lancaster 5c8fa5e586 more documentation 2023-04-26 01:10:45 -05:00
John Lancaster 1aafa1dd11 updated url to point to proxmox 2023-04-26 00:20:29 -05:00
John Lancaster 97df288ff1 renamed volume 2023-04-26 00:20:08 -05:00
John Lancaster 0088021b81 added hello world for testing 2023-04-26 00:19:36 -05:00
John Lancaster 6fd87f1910 switched to docker-compose to make a named volume for the appdaemon data 2023-04-26 00:07:22 -05:00
John Lancaster c64a1f1a0a started documentation page 2023-04-25 23:35:23 -05:00
John Lancaster 02c7cf98ee rest of the pivot 2023-04-25 23:10:44 -05:00
John Lancaster f10927ccce tweak 2023-04-25 22:54:10 -05:00
John Lancaster 34707cf517 added handle_off 2023-04-24 21:33:47 -05:00
John Lancaster 97a6da6815 hopefully simplified the state change callbacks 2023-04-23 23:40:45 -05:00
John Lancaster cf528c5252 added enable feature 2023-04-23 21:56:35 -05:00
John Lancaster 51534fc3ed broke out starting/stopping adjustment callbacks 2023-04-20 23:53:47 -05:00
John Lancaster 92fa257829 added pip cache 2023-04-20 23:49:31 -05:00
John Lancaster b355fb9b4f app setting updates 2023-04-18 22:29:45 -05:00
John Lancaster 3dade9ffb5 added some sleep behavior 2023-04-18 22:25:21 -05:00
John Lancaster e5d3b31e4b most apps switched to controller model 2023-04-18 10:11:39 -05:00
John Lancaster 35a4a3fd0e added sensors 2023-04-18 00:00:12 -05:00
John Lancaster f953d01094 handling lights being in color mode 2023-04-16 09:45:39 -05:00
John Lancaster 798d837660 added SleepSetter 2023-04-15 17:24:41 -05:00
John Lancaster 29e89e42b9 created living_room.yaml 2023-04-15 17:23:38 -05:00
John Lancaster d6bfcc864b WIP adjuster 2023-04-15 17:22:54 -05:00
John Lancaster 1452f53f0a listening only to individual state changes 2023-04-15 17:21:39 -05:00
John Lancaster efb30e3ddc broke out matching_state 2023-04-15 16:32:50 -05:00
John Lancaster f2482b872a checking the light state with each adjustment 2023-04-15 16:23:42 -05:00
John Lancaster c5cc1df425 logging state changes 2023-04-15 16:10:14 -05:00
John Lancaster e301c6838f started ControllerDaylight 2023-04-15 16:05:06 -05:00
John Lancaster 6aa9154a78 controllers start, kind of working 2023-04-15 15:15:37 -05:00
John Lancaster e13fadc5cf migration updates 2023-04-15 15:15:12 -05:00
John Lancaster d32734cc92 added bash to system package 2023-04-12 23:00:39 -05:00
John Lancaster 6f93b9c364 moved Dockerfile 2023-04-12 23:00:16 -05:00
John Lancaster a257d5bde4 added restart behavior to docker_run 2023-04-06 01:05:49 -05:00
John Lancaster 848bed6f88 renamed dev container dockerfile 2023-04-06 01:04:03 -05:00
John Lancaster 40eeb81c1f improved docker_run script for production 2023-04-06 00:56:08 -05:00
John Lancaster 9959545601 passing in ssh credentials from host 2023-04-06 00:55:44 -05:00
John Lancaster ae8c4d4ef9 WIP 2023-03-27 21:11:48 -05:00
John Lancaster dce065a996 created get_next functions 2023-03-26 23:12:04 -05:00
John Lancaster 22ad5180a3 dev container improvements 2023-03-26 23:11:20 -05:00
John Lancaster 164c7f5bbb fixed current_settings 2023-03-19 23:59:06 -05:00
John Lancaster b4624fa97a typo 2023-03-19 23:53:09 -05:00
John Lancaster 6abca10479 seems to be working, but matplotlib display is acting weird 2023-03-19 23:51:34 -05:00
John Lancaster 0c409eabb0 focusing on elevation first 2023-03-19 23:39:39 -05:00
John Lancaster df2a19c2e6 starting point 2023-03-19 23:28:34 -05:00
docker 7ff9b75c88 fixed dev container 2023-03-18 14:56:09 -05:00
docker 6c7b769628 initial commit 2023-03-18 14:36:08 -05:00
58 changed files with 1823 additions and 1153 deletions
+12 -4
View File
@@ -1,5 +1,13 @@
# git.nix __pycache__
*.env *.pyc
.devenv *.png
*.log secrets.yaml
.python-version
*.ipynb
logs/
*.json
namespaces/
+4
View File
@@ -0,0 +1,4 @@
[submodule "apps/room_control"]
path = apps/room_control
url = ssh://gitea/john/room_control
branch = main
-9
View File
@@ -1,9 +0,0 @@
keys:
- &1password age197d424aa7jpj2s735fl2h2s4c687y8vm44usx8wag0r2kh2v7ces4efdyn
- &lola-ad age1qf4c4asf7wcqqyd9aju8fq9dvum4ptcqr8dd6xqengsf6jx7daqqtgup30
creation_rules:
- path_regex: yaml$
key_groups:
- age:
- *1password
- *lola-ad
+10 -85
View File
@@ -1,91 +1,16 @@
# NixOS Configuration for AppDaemon Development # AppDaemon
## Objectives ## Apps
- SSH remote with VSCode ### Room Control
- Debugger must work
- Multiple dev versions (different branches, forks)
- Multiple config directories - deployment and test
- `devenv`-based workflow
- Shell
- Makes `uv` available
- Syncs `devenv` virtual environment
- `appdaemon`
- Build Docker
- Use flakes
- Jupyter through VSCode
- `autoreload` must work with editable install of the dev version
- could always work in a dev container
- Observation - telegraf/promtail
- Utility - portainer, watchtower
## Usage - living_room
- kitchen
- bedroom
- bathroom
### `nfs` ### Cube
Used to rebuild the `ad-nix` system with whatever is currently symlinked to `/etc/nixos` ### Sleep Setter
### `ads` ### TV
Used to enter the development shell. Be careful, as this will create a `.devenv` directory and venv wherever it's entered.
### venv
`.devenv/state/venv/bin/python`
Used in VSCode for type hints, running, and debugging
### Jupyter
- Install devenv kernel - might not be useful?
- `python -m ipykernel install --user --name devenv --display-name "Python (devenv)"`
- Run jupyter notebook on the side with a `uv run jupyter notebook` command
- Use the link with the token to connect the jupyter notebook kernel to it
## Mechanics
### SSH Connection
SSH keys are pre-authorized from `secrets/authorized_keys` which contains the public keys for desktop, laptop, and phone.
### SOPS
- `secrets/secrets.yaml` contains the encrypted keys.
- There needs to be a `~/.config/sops/age/keys.txt` file with the age secret key. This file has to be manually placed.
- `.sops.yaml` indicates to SOPS that the yaml file is encrypted with that secret key.
- `sops-ad` is a convenience script for editing the secrets.yaml file.
## Setup
### Bootstrapping
SSH in to the host as root and get into a shell with `git`.
```shell
nix-channel --update && nix-shell -p git
```
Then build the system from the flake
```shell
nixos-rebuild switch --flake git+https://gitea.john-stream.com/john/ad-nix#ad-nix --impure
```
### Secrets
During build time `/etc/ssh/ssh_host_ed25519_key` automatically gets imported as an age key. If that fingerprint is included in the `.sops.yaml` file, then `secrets/secrets.yaml` can be decrypted during the build. Otherwise `~/.config/sops/age/keys.txt` needs to already be populated.
`secrets/secrets.yaml` needs to be edited from the terminal. There's a `sops-ad` command for convenience. The following keys are required:
- `telegraf_influx_token`
`~/.config/sops/age/keys.txt` needs to be set for the `sops-ad` command to work.
### Tailscale
Needs this in the `/etc/pve/lxc/<vmid>.conf` file on the proxmox host.
```
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
```
-78
View File
@@ -1,78 +0,0 @@
{
"folders": [
{
"path": "/home/appdaemon/ad-lola"
},
{
"path": "/conf/lola"
},
{
"path": "/home/appdaemon/ad-nix"
},
{
"path": "/home/appdaemon/ad-test"
},
],
"settings": {
// Python
"[python]": {
"editor.autoClosingDelete": "always",
"editor.autoClosingBrackets": "always",
"editor.defaultFormatter": "charliermarsh.ruff",
// "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.ruff": "explicit",
// "source.fixAll": "explicit"
}
},
"python.defaultInterpreterPath": "${workspaceFolder}/.devenv/state/venv/bin/python3",
"python.autoComplete.extraPaths": ["~/ad-lola", "${fileWorkspaceFolder}/apps/room_control/src"],
"python.analysis.extraPaths": [
"~/ad-lola",
"${workspaceFolder:conf}/lola/apps/room_control/src",
"${workspaceFolder:conf}/lola/apps/lola-parking/src"
],
"python.analysis.autoFormatStrings": true,
"python.analysis.completeFunctionParens": true,
"python.analysis.autoImportCompletions": true,
"python.analysis.importFormat": "relative",
"python.analysis.autoIndent": true,
"python.analysis.useLibraryCodeForTypes": true,
"python.analysis.languageServerMode": "full",
"python.analysis.typeEvaluation.enableReachabilityAnalysis": true,
"python.languageServer": "Pylance",
"python.terminal.shellIntegration.enabled": true,
// Ruff settings
"ruff.enable": true,
"ruff.organizeImports": true,
"ruff.importStrategy": "fromEnvironment",
"ruff.nativeServer": true,
"ruff.configurationPreference": "filesystemFirst",
"ruff.configuration": "${workspaceFolder}/pyproject.toml",
"ruff.fixAll": true,
"ruff.lint.enable": true,
// https://docs.astral.sh/ruff/rules/
"ruff.lint.extendSelect": [
"F", "W", "I",
"E1", "E2", "E3", "E4", "E5", "E7", "E9"
],
// Notebooks
// "jupyter.askForKernelRestart": false,
"notebook.defaultFormatter": "charliermarsh.ruff",
"notebook.formatOnSave.enabled": true,
"notebook.codeActionsOnSave": {
"notebook.source.fixAll": "explicit",
"notebook.source.organizeImports": "explicit"
},
"editor.rulers": [120],
"editor.wordWrap": "on",
"editor.wordWrapColumn": 120,
// https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
// https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
}
}
Executable
+40
View File
@@ -0,0 +1,40 @@
appdaemon:
uvloop: True
use_dictionary_unpacking: True
# check_app_updates_profile: True
import_method: expert
latitude: 30.250968
longitude: -97.748193
elevation: 150
time_zone: America/Chicago
plugins:
HASS:
type: hass
# ha_url: http://rpi3.home.com:8123
ha_url: http://192.168.1.82:8123
token: !secret long_lived_token
mqtt:
type: mqtt
namespace: mqtt
client_host: zigbee.john-stream.com
client_user: homeassistant
client_password: !secret mqtt_password
client_topics:
- zigbee2mqtt/#
namespaces:
controller:
writeback: hybrid
persistent: true
admin:
api:
http:
url: http://0.0.0.0:5050
logs:
main_log:
date_format: '%Y-%m-%d %I:%M:%S %p'
error_log:
date_format: '%Y-%m-%d %I:%M:%S %p'
+43
View File
@@ -0,0 +1,43 @@
rich_logging:
module: console
global: true
living_room_tv:
module: tv
class: SoundBar
device: BoseTV
remote_entity: remote.broadlink_remote
playing_entity: media_player.sony_google_cast
off_entity: media_player.sony_bravia
scene_detect:
module: scene_detect
class: MotionCanceller
scene: bedsport
room: bedroom
scene_detect2:
module: scene_detect
class: MotionCanceller
scene: in_bed
room: bedroom
leaving:
module: leaving
class: Leaving
person: person.john
apps:
- living_room
- bedroom
- kitchen
- bathroom
- closet
patio:
module: patio
class: Patio
door: binary_sensor.back_contact
light: light.patio
state:
brightness: 150
color_temp: 400
+113
View File
@@ -0,0 +1,113 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass
from daylight_adjuster import DaylightAdjuster
from pvlib.location import Location
from rich import print
import pandas as pd
from astral.sun import elevation, time_at_elevation
from astral import SunDirection
import astral
HOME_TZ = datetime.now().astimezone().tzinfo
@dataclass(init=False)
class Continuous(Hass):
entity: Entity
latitude: float
longitude: float
def initialize(self):
# self.log(f'Brightness Range: {self.brightness_rng}, {self.current_adjuster.get_brightness()}')
# self.run_daily(self.refresh_daylight_times, '00:00:00')
# self.run_every(self.create_img, 'now', int(timedelta(minutes=5).total_seconds()))
self.run_every(self.adjust, 'now', int(timedelta(seconds=30).total_seconds()))
@property
def entity(self) -> Entity:
return self.get_entity(self.args['entity'])
@property
def light_state(self) -> bool:
return self.entity.is_state('on')
@light_state.setter
def light_state(self, new):
if isinstance(new, bool):
if new:
self.entity.turn_on()
else:
self.entity.turn_off()
elif isinstance(new, dict):
self.entity.turn_on(**new)
else:
raise TypeError(f'Wrong type for light state: {new}')
@property
def brightness(self) -> int:
return self.entity.get_state('brightness')
@brightness.setter
def brightness(self, val: int):
self.log(f'Setting brightness of {self.friendly_name(self.args["entity"])} to {val}')
self.entity.turn_on(brightness=val)
@property
def latitude(self) -> float:
return float(self.args['latitude'])
@property
def longitude(self) -> float:
return float(self.args['longitude'])
@property
def location(self) -> Location:
return Location(latitude=self.latitude,
longitude=self.longitude)
@property
def period_df(self):
return pd.DataFrame(self.periods).set_index('time')
# @property
# def full_df(self):
# return pd.concat([df, sub_df], axis=1).sort_index().interpolate().bfill().ffill()
@property
def brightness_rng(self):
for time, brightness_rng in self.periods[::-1]:
if time <= self.time():
return brightness_rng
else:
period_start, brightness = self.periods[-1]
return brightness
@property
def adjuster(self):
return DaylightAdjuster(self.latitude, self.longitude, self.args['periods'])
@property
def sleeping_active(self) -> bool:
if 'sleep_var' in self.args:
return self.get_state(self.args['sleep_var']) == 'on'
else:
return False
def adjust(self, kwargs):
self.log(f'Adjusting...')
self.log(self.adjuster.current_settings)
if self.light_state and not self.sleeping_active:
self.light_state = self.adjuster.current_settings
def create_img(self, kwargs):
self.log(f'Creating daylight curve img...')
try:
# self.current_adjuster.elevation_fig().savefig('/mnt/ha_config/www/daylight_curve.png')
self.adjuster.elevation_fig().savefig('/conf/daylight_curve.png')
except:
raise
else:
self.log(f'Done')
+126
View File
@@ -0,0 +1,126 @@
from enum import Enum
from logging import Logger
from typing import TYPE_CHECKING, Literal, Optional
from appdaemon.plugins.hass.hassapi import Hass
from appdaemon.plugins.mqtt.mqttapi import Mqtt
from pydantic import BaseModel, Field, TypeAdapter, field_validator
from room_control import console
if TYPE_CHECKING:
from room_control import RoomController
class Side(int, Enum):
back = 0
right = 1
top = 2
front = 3
left = 4
bottom = 5
Actions = Literal[
'', 'wakeup', 'slide', 'shake', 'rotate_left', 'rotate_right', 'flip180', 'flip90'
]
class CubeEvent(BaseModel):
action: Optional[Actions] = None
side: Optional[Side] = Field(default=None, ge=0)
action_angle: Optional[float] = None
action_side: Optional[Side] = None
action_from_side: Optional[Side] = None
action_to_side: Optional[Side] = None
battery: Optional[int] = Field(default=None, ge=0)
current: Optional[int] = Field(default=None, ge=0)
device_temperature: Optional[int] = Field(default=None, ge=0)
linkquality: Optional[int] = Field(default=None, ge=0)
power: Optional[int] = Field(default=None, ge=0)
power_outage_count: Optional[int] = Field(default=None, ge=0)
voltage: Optional[int] = Field(default=None, ge=0)
class MQTTResponse(BaseModel):
topic: str
payload: CubeEvent
@field_validator('payload', mode='before')
def payload_str(cls, v: str):
return CubeEvent.model_validate_json(v)
class CallbackEntry(BaseModel):
entity: str
event: Optional[str] = None
type: Literal['state', 'event']
kwargs: str
function: str
name: str
pin_app: bool
pin_thread: int
Callbacks = dict[str, dict[str, CallbackEntry]]
class AqaraCube(Hass, Mqtt):
app: 'RoomController'
logger: Logger
def initialize(self):
self.app: 'RoomController' = self.get_app(self.args['app'])
self.logger = console.load_rich_config(self.app.name, type(self).__name__, level='DEBUG')
topic = f'zigbee2mqtt/{self.args["cube"]}'
self.mqtt_subscribe(topic, namespace='mqtt')
self.listen_event(self.handle_event, 'MQTT_MESSAGE', topic=topic, namespace='mqtt')
self.log(f'Listening for cube events on: [topic]{topic}[/]')
# self.log(f'Number of callbacks: {len(self.callbacks())}')
# for handle, cb in self.callbacks().items():
# self.log(repr(cb))
def terminate(self):
self.log('[bold red]Terminating[/]', level='DEBUG')
def callbacks(self) -> dict[str, CallbackEntry]:
data = TypeAdapter(Callbacks).validate_python(self.get_callback_entries())
name: str = self.name
try:
return data[name]
except KeyError:
return
def handle_event(self, event_name, data, **kwargs):
data = MQTTResponse.model_validate(data)
action = data.payload.action
if action == '' or action == 'wakeup':
return
else:
self.log(
f'{event_name} on [topic]{data.topic}[/], Action: "[yellow]{str(action)}[/]"',
level='DEBUG',
)
if arg := self.args.get(action, False):
self.action_handler(action=action, description=arg)
elif handler := getattr(self, f'handle_{action}', None):
handler(data.payload)
def action_handler(self, action: str, description: str):
self.log(f'{self.args["cube"]}: {action}: {description}')
if description == 'activate':
self.app.activate(cause=f'{self.args["cube"]}: {action}')
elif description.startswith('scene.'):
self.call_service('scene/turn_on', entity_id=description, namespace='default')
self.log(f'Turned on {description}')
elif description.startswith('toggle'):
cause = f'{self.args["cube"]} {action}'
self.app.toggle_activate(kwargs={'cause': cause})
else:
self.log(f'Unhandled action: {action}', level='WARNING')
+9
View File
@@ -0,0 +1,9 @@
cube1:
module: cube
class: AqaraCube
rich: DEBUG
cube: Cube 1
app: bedroom
flip90: toggle
flip180: scene.bedsport
# shake: activate
+186
View File
@@ -0,0 +1,186 @@
import logging
from contextlib import suppress
from dataclasses import InitVar, dataclass, field
from datetime import date, datetime, time, timedelta, tzinfo
from typing import Dict, Iterable
import astral
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from astral import Observer, SunDirection
from astral.sun import elevation, sun, time_at_elevation
from IPython.display import display
HOME_TZ = datetime.now().astimezone().tzinfo
def format_x_axis(fig):
ax: plt.Axes = fig.axes[0]
ax.xaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 2)))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%I%p'))
ax.grid(True)
fig.autofmt_xdate()
def normalize(s: pd.Series, min=None, max=None):
min = min or s.min()
max = max or s.max()
rng = max - min
return ((s - min) / rng) * 100
def get_today_series():
days = pd.date_range(start=datetime.today() - timedelta(days=1), periods=3, freq='1D')
days = days.to_series().dt.date
return days.values
def times_at_elevation(observer: Observer, elevation, direction, days=None):
kwargs = dict(
observer=observer,
elevation=elevation,
direction=direction,
tzinfo=HOME_TZ
)
days = days if days is not None else get_today_series()
df = pd.DataFrame(pd.Series(
data=[time_at_elevation(date=day, **kwargs) for day in days],
index=days,
name='time_at_elevation'
))
df['elevation'] = elevation
df['direction'] = direction
return df
def parse_periods(observer: Observer, periods: Dict, date: date):
for period in periods:
if 'time' in period:
try:
time = datetime.strptime(period['time'], '%I:%M:%S%p').time()
except:
sun_dict = sun(observer=observer, date=date, tzinfo=HOME_TZ)
dt = sun_dict[period['time']]
else:
dt = datetime.combine(date, time, tzinfo=HOME_TZ)
elif 'elevation' in period:
if period['direction'] == 'rising':
dir = SunDirection.RISING
elif period['direction'] == 'setting':
dir = SunDirection.SETTING
assert isinstance(period['elevation'], (int, float))
dt = time_at_elevation(
observer=observer,
elevation=period['elevation'],
date=date,
direction=dir,
tzinfo=HOME_TZ,
)
# res = {'time': dt.replace(tzinfo=None)}
# res = {'time': dt.replace(tzinfo=HOME_TZ)}
res = {'time': dt}
res.update({k: period[k] for k in ['brightness', 'color_temp'] if k in period})
yield res
def elevation_series(observer: Observer, date, **kwargs):
times = pd.date_range(start=date, end=(date + timedelta(days=1)), **kwargs)
elevations = pd.Series(
[elevation(observer, timestamp) for timestamp in times],
index=times, name='elevation')
return elevations
@dataclass
class DaylightAdjuster:
latitude: float
longitude: float
periods: InitVar[Dict]
datetime: datetime = field(default_factory=datetime.now)
resolution: InitVar[int] = field(default=200)
def __post_init__(self, periods: Dict, resolution: int):
self.logger: logging.Logger = logging.getLogger(type(self).__name__)
self.period_df: pd.DataFrame = pd.DataFrame([
p
for date in get_today_series()
for p in parse_periods(self.observer, periods, date)
]).set_index('time').interpolate(method='index').round(0).astype(int)
self.df = self.period_df.join(pd.concat([
elevation_series(self.observer, date, periods=1000, tz=HOME_TZ)
for date in get_today_series()
]), how='outer')
self.df = (
self.df
.sort_index()
.interpolate(method='index')
.bfill().ffill()
# .round(0).astype(int)
# .drop_duplicates()
)
self.df.index = self.df.index.to_series().dt.tz_localize(None)
@property
def observer(self) -> astral.Observer:
return astral.Observer(self.latitude, self.longitude)
@property
def current_settings(self) -> Dict:
now = datetime.now(HOME_TZ)
self.period_df.loc[now] = np.nan
return self.period_df.interpolate(method='index').round(0).astype(int).loc[now].to_dict()
def elevation_fig(self):
fig, ax = plt.subplots(figsize=(10, 7))
elevation = self.df['elevation']
elevation.index = elevation.index.to_series().dt.tz_localize(None)
handles = ax.plot(elevation)
ax.set_ylabel('Elevation')
ax.set_ylim(-100, 100)
format_x_axis(fig)
ax.set_xlim(elevation.index[0], elevation.index[-1])
print(elevation)
# ax.xaxis_date(HOME_TZ)
ax2 = ax.twinx()
handles.extend(ax2.plot(normalize(self.df['brightness'], 1, 255), 'tab:orange'))
handles.extend(ax2.plot(normalize(self.df['color_temp'], 150, 650), 'tab:green'))
ax2.set_ylabel('Brightness')
ax2.set_ylim(0, 100)
handles.append(ax.axvline(datetime.now().astimezone(),
linestyle='--',
color='g'))
# handles.append(ax2.axhline(self.get_brightness(),
# linestyle='--',
# color='r'))
# handles.append(ax.axhline(self.get_elevation(),
# linestyle='--',
# color=handles[0].get_color()))
ax.legend(handles=handles, loc='lower center', labels=[
'Sun Elevation Angle',
'Brightness Setting',
'Color Temp Setting',
'Current Time',
# 'Current Brightness',
# 'Current Elevation'
])
fig.tight_layout()
plt.close(fig)
return fig
+68
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
+99
View File
@@ -0,0 +1,99 @@
from datetime import timedelta
from pathlib import Path
import aiohttp
import yaml
from appdaemon.plugins.hass.hassapi import Hass
def convert_vals_dict(vals):
weather_codes = {
0: 'Unknown',
1000: 'Clear',
1001: 'Cloudy',
1100: 'Mostly Clear',
1101: 'Partly Cloudy',
1102: 'Mostly Cloudy',
2000: 'Fog',
2100: 'Light Fog',
3000: 'Light Wind',
3001: 'Wind',
3002: 'Strong Wind',
4000: 'Drizzle',
4001: 'Rain',
4200: 'Light Rain',
4201: 'Heavy Rain',
5000: 'Snow',
5001: 'Flurries',
5100: 'Light Snow',
5101: 'Heavy Snow',
6000: 'Freezing Drizzle',
6001: 'Freezing Rain',
6200: 'Light Freezing Rain',
6201: 'Heavy Freezing Rain',
7000: 'Ice Pellets',
7101: 'Heavy Ice Pellets',
7102: 'Light Ice Pellets',
8000: 'Thunderstorm',
}
return {
'cloud_coverage': int(round(vals['cloudCover'], 0)),
'condition': weather_codes[vals['weatherCode']],
'humidity': vals['humidity'],
'native_apparent_temperature': vals['temperatureApparent'],
'native_dew_point': vals['dewPoint'],
'native_precipitation_unit': 'in.',
'native_pressure': vals['pressureSurfaceLevel'],
'native_pressure_unit': 'inHg',
'native_temperature': vals['temperature'],
'native_temperature_unit': 'F',
'native_wind_gust_speed': vals['windGust'],
'native_wind_speed': vals['windSpeed'],
'native_wind_speed_unit': 'mph',
'uv_index': vals['uvIndex'],
'wind_bearing': vals['windDirection'],
}
class Weather(Hass):
def initialize(self):
with (Path(self.AD.config_dir) / 'secrets.yaml').open('r') as f:
apikey = yaml.safe_load(f)['tomorrow.io']
self.log('API key loaded')
lat = self.AD.sched.location.latitude
long = self.AD.sched.location.longitude
self.request_kwargs = {
'url': 'https://api.tomorrow.io/v4/weather/forecast',
'params': {
'apikey': apikey,
'location': f'{lat},{long}',
'timesteps': '1h',
'units': 'imperial',
},
}
if loc := self.args.get('location'):
self.request_kwargs['params']['location'] = loc
self.log(f'Updated location to {loc}', level='DEBUG')
interval = timedelta(minutes=5)
self.run_every(self.get_weather_async, 'now', interval.total_seconds())
self.log(f'Getting weather every {interval}')
async def get_weather_async(self, **kwargs):
async with aiohttp.ClientSession() as session:
async with session.get(**self.request_kwargs) as resp:
if resp.status == 200:
self.log('Got weather async', level='DEBUG')
json_data = await resp.json()
await self.publish_temp(json_data)
elif resp.status == 429:
self.log('Rate limited when getting weather', level='WARNING')
else:
self.log(f'Error getting weather async: {resp.status}', level='ERROR')
async def publish_temp(self, json_data):
vals = convert_vals_dict(json_data['timelines']['hourly'][0]['values'])
await self.set_state('weather.tomorrowio', state=vals['condition'], **vals)
+5
View File
@@ -0,0 +1,5 @@
weather:
module: weather
class: Weather
# log_level: DEBUG
location: 78704 US
+49
View File
@@ -0,0 +1,49 @@
import asyncio
from appdaemon.adbase import ADBase
from appdaemon.plugins.hass.hassapi import Hass
class MyClass(Hass):
def initialize(self):
return
def my_callback(self, title: str, message: str, **kwargs) -> None:
self.log(f'{title}: {message}')
self.log(kwargs)
class HelloWorld(ADBase):
def initialize(self):
self.adapi = self.get_ad_api()
def my_callback(self, cb_args: dict) -> None:
self.adapi.log(f'{cb_args["title"]}: {cb_args["message"]}')
async def temp_callback(self, entity, attribute, old, new, **kwargs):
self.log('Temp callback')
temp = await self.get_state('sensor.temperature_nest')
# self.log(json.dumps(temp, indent=4))
self.AD.loop.create_task(self.unreliable_call())
if float(temp) <= kwargs['threshold']:
self.log(f'{entity} is below the threshold')
self.AD.loop.create_task(self.unreliable_call())
self.log('Doing some other, more reliable stuff')
await asyncio.sleep(2.0)
self.log(f'{entity} done')
async def unreliable_call(self):
self.log('Calling unreliable cloud service....')
await asyncio.sleep(5.0)
# await self.call_service('climate/set_temperature', entity_id='climate.living_room', temperature=70)
self.log('Cloud service returned')
def entities_ending_with(self, ending_str: str):
entities = [
entity_id
for entity_id, state in self.get_state().items()
if entity_id.endswith(ending_str)
]
return entities
+3
View File
@@ -0,0 +1,3 @@
HelloWorld:
module: hello
class: HelloWorld
+16
View File
@@ -0,0 +1,16 @@
from appdaemon.plugins.hass.hassapi import Hass
class Leaving(Hass):
def initialize(self):
self.listen_state(self.turn_everything_off, entity_id=self.args['person'], old='home')
def turn_everything_off(self, entity, attribute, old, new, **kwargs):
self.log('turning everything off')
self.log(kwargs)
for app_name in self.args['apps']:
try:
self.get_app(app_name).deactivate(kwargs={'cause': 'leaving'})
except Exception as e:
self.log(f'{type(e).__name__}: {e}')
continue
+27
View File
@@ -0,0 +1,27 @@
from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass
class Patio(Hass):
def initialize(self):
self.door.listen_state(callback=self.handle_door_open, new='on')
self.door.listen_state(callback=self.handle_door_close, new='off')
self.log(f'Patio light initialized for {self.door.friendly_name}')
self.log(f'Sun angle: {self.AD.sched.location.solar_elevation():.1f}')
@property
def door(self) -> Entity:
return self.get_entity(self.args['door'])
@property
def light(self) -> Entity:
return self.get_entity(self.args['light'])
def handle_door_open(self, entity: str, attribute: str, old: str, new: str, **kwargs):
self.log('Door open')
if self.AD.sched.location.solar_elevation() <= 0:
self.light.turn_on(**self.args['state'])
def handle_door_close(self, entity: str, attribute: str, old: str, new: str, **kwargs):
self.log('Door close')
self.run_in(callback=lambda *args, **kwargs: self.light.turn_off(), delay=30.0)
+33
View File
@@ -0,0 +1,33 @@
import logging
from rich.console import Console
from rich.highlighter import NullHighlighter
from rich.logging import RichHandler
def init_logging(log_level: int = logging.INFO):
rich_handler = RichHandler(
console=Console(width=150),
highlighter=NullHighlighter(),
markup=True,
rich_tracebacks=True,
tracebacks_suppress=['pandas', 'discord'],
)
dt_fmt = '%Y-%m-%d %I:%M:%S %p'
# https://docs.python.org/3/library/logging.html#logrecord-attributes
log_format = '[magenta]%(name)s[/]: [cyan]%(funcName)s[/] %(message)s'
root_logger = logging.getLogger()
formatter = logging.Formatter(log_format)
formatter.datefmt = dt_fmt
rich_handler.setFormatter(formatter)
root_logger.addHandler(rich_handler)
root_logger.setLevel(log_level)
# logging.debug(f'Set up logging')
# logging.basicConfig(
# level=log_level,
# format=log_format,
# datefmt=dt_fmt,
# handlers=[rich_handler]
# )
+1
Submodule apps/room_control added at e3186f1b5e
+38
View File
@@ -0,0 +1,38 @@
bathroom:
module: room_control
class: RoomController
loki_url: http://192.168.1.107:3100/loki/api/v1/push
off_duration: '00:05:00'
motion:
sensor: binary_sensor.bathroom_motion_occupancy
ref_entity: light.bathroom
button: Bathroom Button
# rich: DEBUG
states:
- time: '05:00:00'
scene:
light.bathroom:
brightness: 100
color_temp: 250
- time: '12:00:00'
scene:
light.bathroom:
brightness: 175
color_temp: 300
- time: sunset
scene:
light.bathroom:
brightness: 125
color_temp: 350
- time: '23:00:00'
scene:
light.bathroom:
brightness: 50
color_temp: 350
sleep: input_boolean.sleeping
sleep_state:
off_duration: '00:02:00'
scene:
light.bathroom:
brightness: 1
color_temp: 250
+82
View File
@@ -0,0 +1,82 @@
bedroom:
module: room_control
class: RoomController
loki_url: http://192.168.1.107:3100/loki/api/v1/push
off_duration: '00:05:00'
motion:
sensor: binary_sensor.bedroom_motion_occupancy
ref_entity: light.bedroom
button:
- Bedroom Button 1
- Bedroom Button 2
# rich: DEBUG
states:
- time: 'sunrise - 03:00:00'
scene:
light.bedroom:
state: on
color_temp: 200
brightness: 50
light.globe:
state: on
color_temp: 200
brightness: 50
light.overhead:
state: off
- time: '06:00:00'
scene:
light.bedroom:
state: on
color_temp: 250
brightness: 50
light.globe:
state: on
color_temp: 250
brightness: 50
light.overhead:
state: on
color_temp: 250
brightness: 40
- time: '12:00:00'
scene:
light.bedroom:
state: on
color_temp: 325
brightness: 75
light.globe:
state: on
color_temp: 325
brightness: 75
light.overhead:
state: on
color_temp: 325
brightness: 50
- time: 'sunset'
scene:
light.bedroom:
state: on
color_temp: 325
brightness: 50
light.globe:
state: on
color_temp: 325
brightness: 50
light.overhead:
state: on
color_temp: 350
brightness: 65
- time: '01:00:00'
scene:
light.bedroom:
state: on
color_name: green
brightness: 50
light.globe:
state: on
color_name: blue
brightness: 50
light.overhead:
state: on
color_name: blueviolet
brightness: 255
sleep: input_boolean.sleeping
+30
View File
@@ -0,0 +1,30 @@
closet:
module: room_control
class: RoomController
loki_url: http://192.168.1.107:3100/loki/api/v1/push
off_duration: '00:02:00'
motion:
sensor: binary_sensor.closet_motion_occupancy
ref_entity: light.closet
# rich: DEBUG
states:
- time: 'sunrise - 03:00:00'
scene:
light.closet:
brightness: 25
color_temp: 200
- time: 'sunrise'
scene:
light.closet:
brightness: 75
color_temp: 200
- time: '12:00:00'
scene:
light.closet:
brightness: 175
color_temp: 300
- time: sunset
scene:
light.closet:
brightness: 100
color_temp: 400
+42
View File
@@ -0,0 +1,42 @@
kitchen:
module: room_control
class: RoomController
loki_url: http://192.168.1.107:3100/loki/api/v1/push
off_duration: '00:10:00'
motion:
sensor: binary_sensor.kitchen_motion_occupancy
ref_entity: light.kitchen
button: Kitchen Button
# rich: DEBUG
sleep: input_boolean.sleeping
sleep_state:
scene:
light.kitchen:
state: on
brightness: 1
states:
- time: sunrise
scene:
light.kitchen:
state: on
color_temp: 200
brightness: 25
- time: '12:00:00'
scene:
light.kitchen:
state: on
color_temp: 300
brightness: 75
- time: sunset
scene:
light.kitchen:
state: on
color_temp: 450
brightness: 100
- time: '22:00:00'
off_duration: '00:02:00'
scene:
light.kitchen:
state: on
color_temp: 650
brightness: 25
+30
View File
@@ -0,0 +1,30 @@
laundry:
module: room_control
class: RoomController
loki_url: http://192.168.1.107:3100/loki/api/v1/push
off_duration: '00:02:00'
motion:
sensor: binary_sensor.laundry_motion_occupancy
ref_entity: light.laundry
# rich: DEBUG
states:
- time: 'sunrise - 03:00:00'
scene:
light.laundry:
brightness: 25
color_temp: 200
- time: 'sunrise'
scene:
light.laundry:
brightness: 75
color_temp: 200
- time: '12:00:00'
scene:
light.laundry:
brightness: 175
color_temp: 300
- time: sunset
scene:
light.laundry:
brightness: 100
color_temp: 400
+73
View File
@@ -0,0 +1,73 @@
living_room:
module: room_control
class: RoomController
loki_url: http://192.168.1.107:3100/loki/api/v1/push
off_duration: '00:30:00'
motion:
sensor: binary_sensor.living_room_motion_occupancy
ref_entity: light.living_room
button: Living Room Button
door: binary_sensor.front_contact
# rich: DEBUG
states:
- time: sunrise
scene:
light.living_room:
state: on
color_temp: 200
brightness: 75
light.couch_corner:
state: on
color_temp: 200
brightness: 20
- time: '09:00:00'
scene:
light.living_room:
state: on
color_temp: 250
brightness: 130
light.couch_corner:
state: on
color_temp: 250
brightness: 65
- time: '12:00:00'
scene:
light.living_room:
state: on
color_temp: 300
brightness: 255
light.couch_corner:
state: on
color_temp: 450
brightness: 125
- time: sunset
off_duration: 01:00:00
scene:
light.living_room:
state: on
color_temp: 350
brightness: 175
light.couch_corner:
state: on
color_temp: 650
brightness: 25
- elevation: -20
direction: setting
off_duration: 00:30:00
scene:
light.living_room:
state: on
color_temp: 350
brightness: 125
light.couch_corner:
state: on
color_temp: 650
brightness: 5
sleep: input_boolean.sleeping
sleep_state:
# off_duration: '00:02:00'
scene:
light.living_room:
state: 'on'
rgb_color: [255, 0, 0]
brightness: 25
+50
View File
@@ -0,0 +1,50 @@
import asyncio
from appdaemon.plugins.hass.hassapi import Hass
from room_control import RoomController
class SceneDetector(Hass):
def initialize(self):
self.scene_entity = (
self.args['scene']
if self.args['scene'].startswith('scene.')
else f'scene.{self.args["scene"]}'
)
self.scene_entity = self.get_entity(self.scene_entity)
self.listen_event(
self.event_callback, event='call_service', domain='scene', service='turn_on'
)
self.log(f"Waiting for scene '{self.scene_entity.friendly_name}' to activate")
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()
async def scene_detected(self):
self.log(f'Detected scene activation: {self.scene_entity.friendly_name}')
await asyncio.sleep(1.0)
class MotionCanceller(SceneDetector):
@property
def room(self) -> str:
return self.args['room']
async def scene_detected(self):
await super().scene_detected()
callbacks = (await self.get_callback_entries())[self.room]
app: RoomController = await self.get_app(self.room)
for handle, info in callbacks.items():
if info['entity'] == app.motion.sensor_entity_id and 'new=off' in info['kwargs']:
self.cancel_listen_state(handle)
await self.AD.state.cancel_state_callback(handle, app.name)
self.log(f'Cancelled motion callback for {app.name}')
# self.log(json.dumps(info, indent=4))
break
else:
self.log('Did not cancel anything', level='WARNING')
Executable
+161
View File
@@ -0,0 +1,161 @@
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.variable_entity.listen_state(self.handle_state)
self.setup_buttons()
def setup_buttons(self):
if isinstance(self.button, list):
for button in self.button:
self.setup_button(button)
else:
self.setup_button(button)
def setup_button(self, name: str):
topic = f'zigbee2mqtt/{name}'
# self.mqtt_subscribe(topic, namespace='mqtt')
self.listen_event(
self.handle_button,
'MQTT_MESSAGE',
topic=topic,
namespace='mqtt',
button=name,
)
self.log(f'Subscribed: {topic}')
@property
def button(self) -> str:
return self.args['button']
@property
def scene(self) -> str:
res = self.args['scene']
if not res.startswith('scene.'):
res = f'scene.{res}'
return res
@property
def variable(self) -> str:
return self.args['variable']
@property
def variable_entity(self) -> Entity:
return self.get_entity(self.variable)
@property
def state(self) -> bool:
return self.variable_entity.get_state('state') == 'on'
@state.setter
def state(self, new: bool):
state = 'on' if bool(new) else 'off'
self.log(f'Setting {self.variable} to {state}')
return self.variable_entity.set_state(state=state)
@property
def sun_elevation(self) -> float:
state = self.get_state('sun.sun', 'elevation')
assert isinstance(state, float)
return state
def handle_state(self, entity, attribute, old, new, **kwargs):
self.log(f'new state: {self.state}')
if self.state:
self.all_off()
try:
self.call_service('scene/turn_on', entity_id=self.scene)
except Exception:
return
else:
self.log(f'Turned on scene: {self.scene}')
# self.turn_on(self.scene)
def handle_button(self, event_name, data, **kwargs):
# topic = data['topic']
# self.log(f'Button event for: {topic}')
try:
payload = json.loads(data['payload'])
action = payload['action']
except json.JSONDecodeError:
self.log(f'Error decoding JSON from {data["payload"]}', level='ERROR')
except KeyError:
return
else:
self.handle_action(action)
def handle_action(self, action: str):
if action == '':
return
if action == 'hold':
self.log(f' {action.upper()} '.center(50, '='))
self.state = True
elif action == 'double':
self.log(f' {action.upper()} '.center(50, '='))
self.state = not self.state
self.on_apps()
def all_off(self):
self.log('Deactivating apps')
for app_name in self.args['off_apps']:
try:
self.get_app(app_name).deactivate(cause='sleep setter')
except Exception:
self.log(f'Failed to deactivate {app_name}')
continue
self.log('Turning off entities')
for entity in self.args['off_entities']:
try:
self.turn_off(entity)
except Exception:
self.log(f'Failed to turn off {entity}')
continue
def on_apps(self):
if (on_apps := self.args.get('on_apps', None)) is not None:
for app_name in on_apps:
try:
self.get_app(app_name).activate(kwargs={'cause': 'sleep setter'})
except Exception:
return
else:
self.log(f'Activated {app_name}')
+30
View File
@@ -0,0 +1,30 @@
sleep:
module: sleep
class: SleepSetter
# elevation_limit: -10
scene: scene.in_bed
variable: input_boolean.sleeping
button:
- Bedroom Button 1
- Bedroom Button 2
- Living Room Button
- Bathroom Button
off_apps:
# - bedroom
- living_room
- kitchen
- bathroom
- closet
off_entities:
- light.patio
- light.closet
on_apps:
- living_room
- bedroom
sleep_tv:
module: sleep
class: SleepTV
sleep_time: input_datetime.tv_sleep_time
tv: media_player.bedroom_vizio
+125
View File
@@ -0,0 +1,125 @@
from datetime import datetime
from appdaemon.plugins.hass.hassapi import Hass
class Speakers(Hass):
def initialize(self):
self.listen_state(
callback=self.set_volume, entity_id='media_player.nest_minis', new='playing'
)
self.set_volume()
@property
def solar_elevation(self) -> float:
return self.AD.sched.location.solar_elevation(self.get_now())
def set_volume(self, entity=None, attribute=None, old=None, new=None, **kwargs):
self.log('Callback - state changed to playing')
if old == 'paused':
self.log('Unpaused - skipping volume adjust')
return
if self.get_now().time() < datetime.strptime('03:00', '%H:%M').time():
self.log('Setting volume - before 10:00am')
self.call_service(
'media_player/volume_set',
volume_level=0.5,
entity_id=[
'media_player.kitchen_speaker',
'media_player.desk_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.5,
entity_id=[
'media_player.bedroom_speaker',
'media_player.bathroom_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.0,
entity_id=['media_player.patio_speaker'],
)
elif self.get_now().time() < datetime.strptime('10:00', '%H:%M').time():
self.log('Setting volume - before 10:00am')
self.call_service(
'media_player/volume_set',
volume_level=0.7,
entity_id=[
'media_player.kitchen_speaker',
'media_player.desk_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.7,
entity_id=[
'media_player.bedroom_speaker',
'media_player.bathroom_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.0,
entity_id=['media_player.patio_speaker'],
)
elif self.solar_elevation < 0:
self.log('Setting volume - sun below horizon')
self.call_service(
'media_player/volume_set',
volume_level=0.5,
entity_id=[
'media_player.kitchen_speaker',
'media_player.desk_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.3,
entity_id=[
'media_player.bedroom_speaker',
'media_player.bathroom_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.0,
entity_id=['media_player.patio_speaker'],
)
else:
self.log('Setting volume - default case')
self.call_service(
'media_player/volume_set',
volume_level=0.5,
entity_id=[
'media_player.kitchen_speaker',
'media_player.desk_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.5,
entity_id=[
'media_player.bedroom_speaker',
'media_player.bathroom_speaker',
],
)
self.call_service(
'media_player/volume_set',
volume_level=0.0,
entity_id=['media_player.patio_speaker'],
)
+30
View File
@@ -0,0 +1,30 @@
speakers:
module: speakers
class: Speakers
states:
- time: '05:00:00'
sets:
- entity_id:
- media_player.desk_speaker
- media_player.kitchen_speaker
- media_player.bathroom_speaker
volume: 0.7
- entity_id: media_player.bedroom_speaker
volume: 0.7
- entity_id: media_player.patio_speaker
volume: 0.0
- elevation: 0
# - service: media_player.volume_set
# data:
# volume_level: 0.2
# target:
# entity_id:
# - media_player.desk_speaker
# - media_player.kitchen_speaker
# - media_player.bathroom_speaker
# - service: media_player.volume_set
# data:
# volume_level: 0.3
# target:
# entity_id: media_player.bedroom_speaker
+64
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
+3
View File
@@ -0,0 +1,3 @@
traffic:
module: traffic
class: TrafficAlert
+63
View File
@@ -0,0 +1,63 @@
from dataclasses import dataclass, fields
from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass
@dataclass(init=False)
class SoundBar(Hass):
"""Turns on the soundbar by sending a command with the Broadlink remote.
- Turns on when the playing_entity goes to playing
- Turns off when the off_entity turns off
"""
device: str
remote_entity: Entity
playing_entity: Entity
off_entity: Entity
def initialize(self):
self.set_fields()
self.listen_state(self.turn_on_soundbar, self.playing_entity.entity_id, new='playing')
self.log(f'Waiting for {self.playing_entity} to go to playing')
self.listen_state(self.hardware_off_callback, self.off_entity.entity_id, new='off')
self.log(f'Waiting for {self.off_entity} to go to off')
def set_fields(self):
for f in fields(self):
if f.name in self.args:
arg = self.args[f.name]
if f.type == Entity:
if not self.entity_exists(arg):
self.log(f'{arg} does not exist', level='WARNING')
else:
setattr(self, f.name, self.get_entity(arg))
else:
setattr(self, f.name, arg)
else:
self.log(f'{f.name} field is unset', level='WARNING')
self.log(repr(self))
def send_remote_command(self, command: str):
if self.remote_entity.state != 'on':
self.log('Turning on remote')
self.remote_entity.turn_on()
self.log(f'Sending remote command: {command}')
self.call_service(
service='remote/send_command',
entity_id=self.remote_entity.entity_id,
device='BoseTV',
command=command
)
def turn_on_soundbar(self, entity=None, attribute=None, old=None, new=None, **kwargs):
self.log(f'{self.playing_entity} is playing')
self.send_remote_command(command='TV')
def hardware_off_callback(self, entity=None, attribute=None, old=None, new=None, **kwargs):
self.log(f'{self.off_entity} is off')
self.send_remote_command(command='power')
-75
View File
@@ -1,75 +0,0 @@
{ pkgs, lib, userSettings, systemSettings, ... }:
{
imports = [
# (import ./home-manager {inherit systemSettings userSettings lib pkgs;})
./nixos
./scripts
];
system.stateVersion = systemSettings.stateVersion;
time.timeZone = "${systemSettings.timeZone}";
nix.settings.trusted-users = [ "root" "@wheel" ];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.download-buffer-size = 524288000; # 500MB
programs.nix-ld.enable = true;
sops.defaultSopsFile = ./secrets/encrypted_secrets.yaml;
sops.defaultSopsFormat = "yaml";
# This is needed for nix to access the secrets at build time.
# It doesn't affect for the `sops ...` command
# Optional if the system has the key age for /etc/ssh/ssh_host_ed25519_key in .sops.yaml
# sops.age.keyFile = "${userSettings.adHome}/.config/sops/age/keys.txt";
environment.systemPackages = with pkgs; [
sops
gdbm
];
virtualisation.docker.enable = true;
virtualisation.oci-containers.backend = "docker";
services.vscode-server.enable = true;
services.openssh.enable = true;
services.tailscale.enable = true;
# services.cron = {
# enable = true;
# systemCronJobs = [
# "30 2 * * * /run/current-system/sw/bin/nfsu > /etc/nixos/auto_update.log 2>&1"
# ];
# };
# systemd.timers."auto-update" = {
# wantedBy = [ "timers.target" ];
# timerConfig = {
# OnCalendar="*-*-* 4:00:00";
# Unit = "auto-update.service";
# };
# };
# systemd.services."auto-update" = {
# script = ''
# ${pkgs.coreutils}/bin/echo "Running auto-update"
# FLAKE=$(${pkgs.coreutils}/bin/readlink -f /etc/nixos)
# ${pkgs.coreutils}/bin/echo "FLAKE: $FLAKE"
# ${pkgs.nix}/bin/nix flake update --flake $FLAKE --impure
# ${pkgs.git}/bin/git -C $FLAKE add "$FLAKE/flake.lock" > /dev/null 2>&1
# ${pkgs.sudo}/bin/sudo ${pkgs.nixos-rebuild}/bin/nixos-rebuild switch --flake $FLAKE#${systemSettings.hostName} --impure
# '';
# serviceConfig = {
# Type = "oneshot";
# User = "${userSettings.userName}";
# };
# };
# https://nixos.wiki/wiki/Storage_optimization
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
nix.optimise.automatic = true;
nix.optimise.dates = [ "Mon *-*-* 05:00:00" ];
}
+31
View File
@@ -0,0 +1,31 @@
include:
- docker-observation/docker-compose.yml
services:
appdaemon:
container_name: appdaemon
image: acockburn/appdaemon:dev
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- config:/conf
- logs:/logs
ports:
- 5050:5050
restart: unless-stopped
tty: true
volumes:
config:
driver: local
driver_opts:
o: bind
type: none
device: ./
logs:
driver: local
driver_opts:
o: bind
type: none
device: ./logs
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
SCRIPT_PATH=$(readlink -f ${BASH_SOURCE:-$0})
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
DOCKER_BUILDKIT=1 docker build -t appdaemon:custom -f $SCRIPT_DIR/.devcontainer/Dockerfile $SCRIPT_DIR
+83
View File
@@ -0,0 +1,83 @@
# Appdaemon Apps
## Room Controller
Formerly `BasicMotion` and others
```mermaid
graph TD
Motion --> Room
Button --> Room
Door --> Room
```
### Behaviors
#### Button
| Action | Response |
|--------------|---------------------------------------------|
| Single-press | Toggle the room state (activate/deactivate) |
| Long-press | |
#### Door
Activates the room if the door opens when everything in the room is off.
### Config
#### Required
| Key | Behavior |
|----------|----------------------------------------------|
| `entity` | Main entity for the room |
| `sensor` | `binary_sensor` (motion) sensor for the room |
| `scene` | List of states and times for the room. |
| `sleep` | [input_boolean] of the sleep mode variable |
[input_boolean]: https://www.home-assistant.io/integrations/input_boolean/
??? example "Example state for `scene` key"
```yaml
- time: 22:00:00
off_duration: 00:02:00 # (1)
scene:
light.kitchen:
state: on
color_temp: 650
brightness_pct: 10
```
1. (Optional) overrides the default `off_duration` for this period only
#### Optional
| Key | Behavior |
|----------------|-----------------------------------------------------------------------------------------|
| `off_duration` | Default time for the motion sensor to be clear before deactivating the room. `HH:MM:SS` |
| `button` | ID of the button to control the room |
| `door` | `binary_sensor` (door) sensor for the room |
??? example "Sample Button Event Data"
```yaml hl_lines="3"
event_type: deconz_event
data:
id: living_room
unique_id: 00:15:8d:00:06:ba:29:70
event: 1002
device_id: fc0ad75dfb8d3a55abfe842199cc94e9
origin: LOCAL
time_fired: "2023-04-26T05:40:39.762306+00:00"
context:
id: 01GYY17ZPJVP57C8PN1K3R4D7J
parent_id: null
user_id: null
```
## Sleep Setter
`sleep.py`
### Config
+14
View File
@@ -0,0 +1,14 @@
# Migration
Migration instructions for devices from deCONZ to Zigbee2MQTT
## Process
1. Delete device in deCONZ
2. Reload the deCONZ integration
3. Fire the `deconz.remove_orphaned_entities` service
4. Restart Home Assistant platform. Restarting the whole system isn't necessary.
5. Factory reset device (varies per device)
6. Pair with Zigbeet2MQTT
7. Rename device and apply to Home Assistant
8. Rename entity in AppDaemon
Generated
-397
View File
@@ -1,397 +0,0 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": [
"devenv"
],
"flake-compat": [
"devenv"
],
"git-hooks": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"owner": "cachix",
"repo": "cachix",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1756048064,
"narHash": "sha256-mVgB6qWhLrCW6AciLyFXosDKKZFtBgqvixcA8a07s+g=",
"owner": "cachix",
"repo": "devenv",
"rev": "3fb20c149d329b01a2b519fbb2a9ca3e6e6e1b05",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1750779888,
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1756022458,
"narHash": "sha256-J1i35r4HfNDdPpwL0vOBaZopQudAUVtartEerc1Jryc=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "9e3a33c0bcbc25619e540b9dfea372282f8a9740",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nix-home",
"nixpkgs"
]
},
"locked": {
"lastModified": 1750033262,
"narHash": "sha256-TcFN78w6kPspxpbPsxW/8vQ1GAtY8Y3mjBaC+oB8jo4=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "66523b0efe93ce5b0ba96dcddcda15d36673c1f0",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-parts": "flake-parts",
"git-hooks-nix": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
]
},
"locked": {
"lastModified": 1755029779,
"narHash": "sha256-3+GHIYGg4U9XKUN4rg473frIVNn8YD06bjwxKS1IPrU=",
"owner": "cachix",
"repo": "nix",
"rev": "b0972b0eee6726081d10b1199f54de6d2917f861",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "devenv-2.30",
"repo": "nix",
"type": "github"
}
},
"nix-home": {
"inputs": {
"home-manager": "home-manager_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1750825763,
"narHash": "sha256-gUtcO/8Bcw4YerJpSIRu+Q2MYKxWrtT+8Bp3Mh1Qfmw=",
"ref": "refs/heads/main",
"rev": "1ab1e4b9e610dcd40a3d728f377b6ac8a302d977",
"revCount": 26,
"type": "git",
"url": "https://gitea.john-stream.com/john/nix-home"
},
"original": {
"type": "git",
"url": "https://gitea.john-stream.com/john/nix-home"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1755615617,
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-python": {
"inputs": {
"flake-compat": "flake-compat_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1755249745,
"narHash": "sha256-lDIbUfJ8xK62ekG+qojTlA1raHpKdScBTx8IFlQYx9U=",
"owner": "cachix",
"repo": "nixpkgs-python",
"rev": "b6632af2db9f47c79dac8f4466388c7b1b6c3071",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "nixpkgs-python",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1682134069,
"narHash": "sha256-TnI/ZXSmRxQDt2sjRYK/8j8iha4B4zP2cnQCZZ3vp7k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd901ef4bf93499374c5af385b2943f5801c0833",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"home-manager": "home-manager",
"nix-home": "nix-home",
"nixpkgs": "nixpkgs",
"nixpkgs-python": "nixpkgs-python",
"sops-nix": "sops-nix",
"vscode-server": "vscode-server"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1754988908,
"narHash": "sha256-t+voe2961vCgrzPFtZxha0/kmFSHFobzF00sT8p9h0U=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "3223c7a92724b5d804e9988c6b447a0d09017d48",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"vscode-server": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1753541826,
"narHash": "sha256-foGgZu8+bCNIGeuDqQ84jNbmKZpd+JvnrL2WlyU4tuU=",
"owner": "nix-community",
"repo": "nixos-vscode-server",
"rev": "6d5f074e4811d143d44169ba4af09b20ddb6937d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-vscode-server",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
-141
View File
@@ -1,141 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
vscode-server.url = "github:nix-community/nixos-vscode-server";
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs-python = {
url = "github:cachix/nixpkgs-python";
inputs.nixpkgs.follows = "nixpkgs";
};
devenv = {
url = "github:cachix/devenv";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-home = {
url = "git+https://gitea.john-stream.com/john/nix-home";
inputs.nixpkgs.follows = "nixpkgs";
};
};
nixConfig = {
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
extra-substituters = "https://devenv.cachix.org";
};
outputs = { self, ... }@inputs:
let
inherit (self) outputs;
nixosSystem = inputs.nixpkgs.lib.nixosSystem;
userSettings = {
userName = "appdaemon";
adHome = "/home/appdaemon";
};
systemSettings = {
hostName = "ad-nix";
stateVersion = "24.05";
system = "x86_64-linux";
timeZone = "America/Chicago";
locale = "en_US.UTF-8";
pythonVersion = "3.12"; # This is largely irrelevant because uv will handle it
};
pkgs = inputs.nixpkgs.legacyPackages.${systemSettings.system};
in
{
nixosConfigurations.${systemSettings.hostName} = nixosSystem {
system = systemSettings.system;
specialArgs = {
inherit inputs;
inherit systemSettings;
inherit userSettings;
};
modules = [
(inputs.nixpkgs + "/nixos/modules/virtualisation/proxmox-lxc.nix")
inputs.home-manager.nixosModules.default
inputs.nix-home.nixosModules.default { user = "${userSettings.userName}"; }
inputs.vscode-server.nixosModules.default
inputs.sops-nix.nixosModules.sops
./configuration.nix
];
};
# https://devenv.sh/guides/using-with-flakes/#the-flakenix-file
packages.${systemSettings.system} = {
devenv-up = self.devShells.${systemSettings.system}.default.config.procfileScript;
devenv-test = self.devShells.${systemSettings.system}.default.config.test;
};
devShells.${systemSettings.system}.default = inputs.devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
({ pkgs, config, ... }: {
# This is your devenv configuration
# https://devenv.sh/reference/options/#pre-commithooks
git-hooks.hooks = {
end-of-file-fixer.enable = true;
trim-trailing-whitespace.enable = true;
};
# https://devenv.sh/supported-languages/python/
languages.python = {
enable = true;
version = systemSettings.pythonVersion;
uv = {
enable = true;
package = pkgs.uv;
sync = {
enable = true;
allExtras = true;
arguments = [ "-U" ];
};
};
};
packages = with pkgs; [
git
gdbm
# (python312.withPackages (python-pkgs: with python-pkgs; [ gdbm ]))
(python312.withPackages (python-pkgs: with python-pkgs; [
gdbm
notebook # kinda hacky, but needed so that jupyter notebook has some shared library it needs?
]))
(writeShellScriptBin "docs" "${pkgs.uv}/bin/uv run sphinx-autobuild -E ./docs/ ./docs_build --port 9999")
(writeShellScriptBin "ab" "${pkgs.uv}/bin/uv build --wheel --refresh")
(writeShellScriptBin "adb" "ab && ${pkgs.docker}/bin/docker build -t acockburn/appdaemon:local-dev .")
# (writeShellScriptBin "ad-nb" "cd $(readlink -f /etc/nixos) && devenv up")
];
# processes = {
# my-jup.exec = "uv run jupyter notebook";
# };
enterShell = ''
alias fix="${pkgs.uv}/bin/uv run ruff check --fix"
alias appdaemon="${pkgs.uv}/bin/uv run --frozen appdaemon"
# alias ad="appdaemon"
export PS1="\[\e[0;34m\](AppDaemon)\[\e[0m\] \[\033[1;32m\][\[\e]0;\u@\h: \w\a\]\u@\h:\w]\$\[\033[0m\] "
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
echo -e "URL: \e[34m$(${pkgs.git}/bin/git config --get remote.origin.url)\e[0m"
echo -e "Branch/Tag: \e[32m$(${pkgs.git}/bin/git describe --tags --exact-match 2>/dev/null || ${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)\e[0m"
echo -e "Hash: \e[33m$(${pkgs.git}/bin/git rev-parse --short HEAD)\e[0m"
echo "AppDaemon v$(${pkgs.uv}/bin/uv pip show appdaemon | awk '/^Version:/ {print $2}') development shell started"
'';
})
];
};
};
}
-8
View File
@@ -1,8 +0,0 @@
{ ... }:
{
imports = [
./home-manager.nix
./docker
./services
];
}
-7
View File
@@ -1,7 +0,0 @@
{ ... }:
{
imports = [
./portainer.nix
./watchtower.nix
];
}
-13
View File
@@ -1,13 +0,0 @@
{ config, ... }:
{
virtualisation.oci-containers.containers.portainer-agent = {
image = "portainer/agent:latest"; # Use the latest Portainer agent image
ports = [
"9001:9001" # Expose the Portainer agent API port
];
volumes = [
"/etc/zoneinfo/${config.time.timeZone}:/etc/localtime:ro"
"/var/run/docker.sock:/var/run/docker.sock"
];
};
}
-11
View File
@@ -1,11 +0,0 @@
{ config, ... }:
{
virtualisation.oci-containers.containers.watchtower = {
image = "containrrr/watchtower:latest";
volumes = [
"/etc/zoneinfo/${config.time.timeZone}:/etc/localtime:ro"
"/var/run/docker.sock:/var/run/docker.sock"
];
environment = {WATCHTOWER_SCHEDULE = "0 0 3 * * *";};
};
}
-27
View File
@@ -1,27 +0,0 @@
{ lib, pkgs, systemSettings, userSettings, ... }:
{
security.sudo-rs = {
enable = true;
execWheelOnly = false;
wheelNeedsPassword = false;
};
users.users.${userSettings.userName} = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" ];
};
home-manager = {
useGlobalPkgs = true;
users.${userSettings.userName} = {
home.stateVersion = systemSettings.stateVersion;
home.homeDirectory = lib.mkForce "${userSettings.adHome}";
systemd.user.startServices = "sd-switch"; # helps with handling systemd services when switching
programs.gh.enable = true;
programs.git.extraConfig.safe.directory = "/home/appdaemon/ad-nix";
home.packages = with pkgs; [
lazydocker
];
};
};
}
-8
View File
@@ -1,8 +0,0 @@
{ ... }:
{
imports = [
./promtail.nix
./telegraf.nix
./restic.nix
];
}
-86
View File
@@ -1,86 +0,0 @@
{ config, pkgs, ... }:
let
lokiHost = "https://loki.john-stream.com";
in
{
systemd.services.promtail.serviceConfig = {
SupplementaryGroups = [ "docker" ];
};
environment.systemPackages = with pkgs; [
(pkgs.writeShellScriptBin "promtail-check" "systemctl status promtail.service")
(pkgs.writeShellScriptBin "promtail-watch" "journalctl -u promtail.service -b -n 25 -f")
];
services.promtail = {
enable = true;
configuration = {
server = {
http_listen_port = 3031;
grpc_listen_port = 0;
};
positions = {
filename = "/tmp/positions.yaml";
};
clients = [{url = "${lokiHost}/loki/api/v1/push";}];
scrape_configs = [
{
job_name = "journal";
journal = {
max_age = "24h";
path = "/var/log/journal";
json = true;
# matches: _TRANSPORT=kernel;
labels = {
job = "systemd-journal";
host = config.networking.hostName; # Dynamically fetch the hostname
};
};
relabel_configs = [
{
source_labels = [ "__journal__systemd_unit" ];
target_label = "unit";
}
];
}
{
job_name = "flog_scrape";
docker_sd_configs = [
{
host = "unix:///var/run/docker.sock";
refresh_interval = "5s";
}
];
relabel_configs = [
{
source_labels = [ "__meta_docker_container_name" ];
regex = "/(.*)";
target_label = "container";
}
{
source_labels = [ "__meta_docker_container_label_com_docker_compose_oneoff" ];
target_label = "oneoff";
}
{
source_labels = [ "__meta_docker_container_label_com_docker_compose_project_config_files" ];
target_label = "compose_file";
}
{
source_labels = [ "__meta_docker_container_label_com_docker_compose_project" ];
target_label = "project_name";
}
{
source_labels = [ "__meta_docker_container_label_com_docker_compose_service" ];
target_label = "service";
}
{
target_label = "host";
replacement = "${config.networking.hostName}";
}
];
}
];
};
# extraFlags
};
}
-49
View File
@@ -1,49 +0,0 @@
{ config, pkgs, userSettings, ... }:
{
sops.secrets.restic_password = {
owner = config.users.users.${userSettings.userName}.name;
mode = "0440";
};
environment.systemPackages = with pkgs; [
restic
(pkgs.writeShellScriptBin "restic-backup" "sudo systemctl start restic-backups-localBackup.service")
(pkgs.writeShellScriptBin "restic-backup-check" "sudo journalctl -b -u restic-backups-localBackup.service")
];
environment.variables = {
RESTIC_REPOSITORY = "/mnt/restic/appdaemon";
RESTIC_PASSWORD = "${builtins.readFile config.sops.secrets."restic_password".path}";
};
services.restic.backups = {
localBackup = {
repository = "/mnt/restic/appdaemon";
passwordFile = config.sops.secrets."restic_password".path;
initialize = true;
timerConfig = {
OnCalendar = "03:00";
RandomizedDelaySec = "2h";
Persistent = true;
};
paths = [
"/home"
"/conf"
"/etc/nixos"
"/etc/ssh" # necessary for SOPS nix to have the same keys
];
exclude = [
".cache"
".vscode*"
".devenv"
".venv"
"build"
"dist"
"__pycache__"
"*.egg-info"
"namespaces"
];
};
};
}
-57
View File
@@ -1,57 +0,0 @@
{ config, pkgs, ... }:
let
influxURL = "https://influxdb.john-stream.com";
organization = "homelab";
bucket = "docker";
token = "${builtins.readFile config.sops.secrets."telegraf_influx_token".path}";
in
{
sops.secrets."telegraf_influx_token" = { };
environment.systemPackages = with pkgs; [
(pkgs.writeShellScriptBin "telegraf-check" "systemctl status telegraf.service")
(pkgs.writeShellScriptBin "telegraf-watch" "journalctl -u telegraf.service -b -n 25 -f")
];
systemd.services.telegraf = {
environment.INFLUX_WRITE_TOKEN = token;
serviceConfig.SupplementaryGroups = [ "docker" ];
};
services.telegraf = {
enable = true;
extraConfig = {
agent = {
interval = "10s";
round_interval = true;
metric_batch_size = 1000;
metric_buffer_limit = 10000;
collection_jitter = "0s";
flush_interval = "10s";
flush_jitter = "0s";
precision = "";
hostname = "";
omit_hostname = false;
};
inputs = {
docker = {
endpoint = "unix:///var/run/docker.sock";
gather_services = false;
source_tag = false;
container_name_include = [];
timeout = "5s";
perdevice_include = ["cpu" "blkio" "network"];
docker_label_include = [];
};
};
outputs = {
influxdb_v2 = {
urls = ["${influxURL}"];
token = "$INFLUX_WRITE_TOKEN";
organization = "${organization}";
bucket = "${bucket}";
};
};
};
};
}
+8
View File
@@ -0,0 +1,8 @@
# --extra-index-url https://www.piwheels.org/simple
# pvlib --only-binary=:pvlib:
# matplotlib --only-binary=:matplotlib:
# jupyterlab --only-binary=:jupyterlab:
rich
pydantic
# /conf/apps/room_control
-9
View File
@@ -1,9 +0,0 @@
{ pkgs, systemSettings, ... }:
{
environment.systemPackages = with pkgs; [
(pkgs.writeShellScriptBin "ads" "nix develop --no-pure-eval $(readlink -f /etc/nixos)")
(pkgs.writeShellScriptBin "link-nix" "${builtins.readFile ./link-nix.sh}")
(pkgs.writeShellScriptBin "sops-ad" "sops $(readlink -f /etc/nixos)/secrets/encrypted_secrets.yaml")
(pkgs.writeShellScriptBin "lola-up" "docker compose -f /conf/lola/docker-compose.yml up -d")
];
}
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -e
if [ "$#" -eq 0 ]; then
echo "Error: No arguments provided."
echo "Usage: $0 <path>"
exit 1
fi
CURRENT=$(readlink -f /etc/nixos)
if [ -d "/etc/nixos" ]; then
sudo rm -r /etc/nixos
fi
echo "Unlinked $CURRENT"
sudo ln -s $1 /etc/nixos
echo "Linked $(readlink -f /etc/nixos)"
-3
View File
@@ -1,3 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDIaHS9CLiDc6T1tja0fBwwv5qc6iwuckWN6w8MNo5yiR8LPWyrfueowNJkL4HMqu6OEuggtdybGw1Do4sW5o+toHCyWfZf3khI1l15opPXuVpD4CWED+SpJiZ0wBgRaCaWhfxLI+s4JOhjOO2OjiClPX3HfxIHyTpRiR78lOMcIieHSnzrAV2MatYKf6lL2ckOsIPwxo/OVM+1ljjX+HLq9IxGUCpWOnF4nF1rq3gKL2JUh2KsrgrzE3NB7EFuqKm8F0tF2rG3JjSvlwox0h06drKD02lpZWXPOBRlcyFDpNXymmc2bpG0S2Bbj5g+pqNBB0jO0h3kzWvYYqrtU/ElObg1cXhyi0PFOhhptlbhbK0Ao8B+pAbSZ661nMT3jpRWLVbnJrRFnXXdjX08r5eseQ3k4CFpv+g64n7yg3IMo9f8gA9P/hOexR+qu5AQ1Ad/tvkp6pPXnR/zsUnbe4p2A9MaNJm4E1zxbs5VGlXynNikXwDL+spkrnjwdfUULTk= john@JOHN-PC
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFn5ilhqaeDsOWSk7y29se2NvxGm8djlfL3RGLokj0q6 john@john-p14s
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHh9SBuxU2dOJHnpGZAE4cwe0fXcTBBAx+JmRsmIX+Tk8zooeM32vbNxxSXiZNpBGH5wzHNb534dWexGGG3sOaONmcL7SCoPIvaAdnIn5VsiznerLrzppSbx3Qn8eyF97WAGCcOcIUNmTIDDx1m6zG762WQnoaUEy0Ul5IR7ET5GQxP3p5Qwx8yqfixKDwarvV421sUIxYt9gee31jS9jcI3MFd6EL57hWle95Z8BGpR/Q7sXDBTZQWMZauh5NPwLMZS7k3bHgxXZ7WNOw/J/yts1ckBbvIFJSRNnMuWD0oGnDTL6aivGi+Eiswp0fpKzYGzquB3/wr3VU4G1JcMM5 JuiceSSH
-31
View File
@@ -1,31 +0,0 @@
telegraf_influx_token: ENC[AES256_GCM,data:XHT7lvRrw9MeC0Jxe2EYTTa/iB5QLVTzp9TDJaljssRR+kGdK3va1u14NX5b6jFrHnAXLiMdMQ5UTdbsnYH43TnRkY29mcVHxwaQv+rbCgEIKOAYFeIw0g==,iv:uzBYXWYRDH6bHZ3pubWh5Qn/2dN2Rz+sjEmrqpKhA4o=,tag:wemgU05aTl9S1rwt+fVQug==,type:str]
restic_password: ENC[AES256_GCM,data:8ArxlyulTejzZ2eA9LqLptAZdBfBZJpeNmaw7r9H2ZPQsPAuT4uMcGRgvYF3tD1d9msyUC5yFy5trQfUxUMhXUrnPnFgZEYUrq+BVG/VraYjH74N1YTSKHksz7kqEGmTpMh6DpNLSq3JWfUT/T2GWDhIjRfQf1O++nlAWHDLT0+aEPT633+o63k8+GZuC38Khsv22dYHki1U57QQusk8x5Rj/0ZwuftJe7ItKs28nXZyLejFq+c+OTvJxjQb70FvHY0QfrGFA8RPteJgoLuTDrnKtkw2CuTpfnfhQXQw9oxUnT6x3L34RMY6Tla1PZt/xp07VZC1vnmmLB1prwJtUgvtQfzp8hLVNipoNAfg9ujg7eTq0Dwm7yHVe5hSkOxp1qh47mwA7Og9yb0t7FZE3ZiVT0P5dH/+Qvp76KhCCQUDA4ttKKw8TNjsiqrcEMyDBj+c1mXRpcXfgoAhJpgtEgHT28GEwVl927Zz3zpGG23Gu9pn5ow09GRWJOzvInwnEI/3Thz+DKySzKB2xDK/Nu8hXsmF37iYafLBSnDRGw8RPp6DQBBFNT84WlncCz71yNt7diiAmdxFVESIv3P0sXMVEXEJ3yx/8ke/2quGiCvYUfvxFsU8xsPIkA3gQodHJY+8q16kjafuMUZkuhZxhFZuScKEThlSkJcvlX+C19dlJYbQM/NoLN8KEeJ+ULoTd+CgGtlK7gXEE9GC5i+rTW/9pZrsg8MaPJAxw/jnZO9ikxJtxRPiJGJ7r2zkEwM9DdlPNlbPWw3BQO/ikOS6UhtcuQI2dj0KJC1b4jzrHCALa7h4tRZmT40isEC1KuGcj02CR0QFAVYWWCz9S6TE2RR8y+OiEJosRfTWFg1CbWyGjBAsZ8WbyAhufB6EhF2F5CA3bMnGc1D6JAgrQw0rowce1Znzs2hYQP4ysaDMLoUhRfDjdfUlb9byOeRwKnLFxg67mAHO1ON8DUT+ZnLeNH3rEmams5g9irtLcH3nxV45ESZfFDqVLr645gmfPNs92a8K1ZUwU3XpO6mOR7+iLiKpFIhlVavtsJPKE1XAxnhilH6Hekd78PAdKc+aZLeDGHUZcTZYEN76mxRfBe20GpkUlx9ekHN4ZP1qvQl301vefNpmfflcJheJ/fgsLjDW5US5hyG6PZ/+GG1xAEhBEmSKH9GvN7sk+FwaxdVSzZwcL7Dj2fyX89kSWoZdKMbE6RwsUHNo1Pdfc1w1M8RxSnOLZL7ijG/nJFLn0T6I5cjnKQ+qscsRQz+62aUczTRp80jFHnNo1VOrsO9Oz0YX1/t9Pnq1mR/X/UzZFwiRKrdLacTwRSEWUFi5zf4PX5rcd6EeZbp5DuvP7RZt39npAmzNz+TBKZP06RymG5CdDVzO41xNJWt7bQ/sd3tw3MUXJaz87Glbg7xq+5EdoUE9J/PaJfO5i+9CAaKCOiFBUfr/NjWv3zpfMCZ50HzqlsQCLnOMWS8S65V5xh2a1h77sOYsrWESvUcdg1ryfGd98znyNE5P7BEBo83P8DIKdYrRoPaWPWHRC6ldOOvFDEeaHy6GedV6k0qKcueGHNYRHG4aMOV9lnlc3qMOpXkNFWk45Mv5pmFgGlgrmutjXzi3ljxa0cGe5UUdPbr4lDZ+U2yPvCOu7Lg7FpVyCR77QMerF0xENKrhB4G57QBUhtx8TUdYH/B6+vka1LiKz/i09VEDiQ2K1q5831hUfrS6GyXN8p7JdhCQ3C66V8cMpb9/llO06LLV5Cc4ooB0svSigMuQ4UFDO5bTkrt7wGJLG/mHrb8DjDPI5nuWhyoxs6OqCvg2t2/HJwK9,iv:wiW/f7wLO7kfd3CKDfoYZnXj697qIFRokAut7VXALVM=,tag:VzKJ/BrCL6zNbglsDqJx9w==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age197d424aa7jpj2s735fl2h2s4c687y8vm44usx8wag0r2kh2v7ces4efdyn
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAralB5SnNwckUxMkRad0Jr
U2xMRVpnUU9GNHpZTEtLdnpwc2tmT08rQmpNCnFzdHpOdWFpZzBNR1lUSHR5U3lr
Vk1HTEQ3REFvdUg1T0hMM014N3BtcVEKLS0tIE5LUTF4Qk1XSXlNNkxNN2pnVi9P
TXd3eUJyYTZYaENSV3FEU2pGbFc1RDQKMj8dOska8lpMAFKV2w6bbO/r01K/9Dw5
Q/jp5XdYtyaGSZcxRnHHbJYldyKKYII9Rcm/uDNuMNA/gCFvbSLccA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1qf4c4asf7wcqqyd9aju8fq9dvum4ptcqr8dd6xqengsf6jx7daqqtgup30
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuWmtMR1Vuc0g2OCsxSjJ2
eCs1bWJyT3g3QktRRHlzVFB6bkZjVjIwRlRFCmVtam9HWWErdlVwYkFrSUprZHR4
bllDcWdCT2ZiRFpaQ1lVZVBSb05kb2MKLS0tIGgrRUx4TTljdDVGVCtxN0kyZGRL
Vm1ldGhPRmNyZHErekRlbFBZQy8wK0EKY2vsWzqtX5w4vM0aLGEN2ZO0Rm9slcKk
6Yx2KvJAT6dNg2lqjzXYYS/MvnpOrW6fA46bmWKaAl9IzKhyW+2avw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-02-17T06:15:17Z"
mac: ENC[AES256_GCM,data:zbnP62SqnI7UUb5lP4UlgzWPDkUegvVX2lAbRcDqWqZJsXTkRPefdUIFPO3aZn2EW0aKlFQGEwARTtOtQ9hLYhbqcvAvh5Ur5eFh3szp9ejgF59JBdYGH8PTR/6FkCaVnyuMA1t3940gVhs8eIRdfdjihTHsIe254/3xzBtVG4o=,iv:j7EImL80FgAt7bjlkgB5KIKduKniUaoyz8fnHr/v2rM=,tag:5vK0s6Qf6t2HRhDPaZkT6Q==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4
-37
View File
@@ -1,37 +0,0 @@
{ pkgs ? import <nixpkgs> {}, unstable ? import <nixpkgs-unstable> {} }:
pkgs.mkShell {
packages = [
pkgs.git
(pkgs.python312.withPackages (python-pkgs: with python-pkgs; [
pip
setuptools
wheel
# pyproject deps
aiohttp
astral
bcrypt
deepdiff
feedparser
iso8601
paho-mqtt
requests
uvloop
pydantic
click
]))
];
shellHook = ''
echo "Welcome to the Nix shell for AppDaemon development"
cd /usr/src/app
echo "URL: $(git config --get remote.origin.url)"
echo "Branch: $(git rev-parse --abbrev-ref HEAD)"
echo "Hash: $(git rev-parse --short HEAD)"
alias build="uv run python -m build"
alias dbuild="docker build -t acockburn/appdaemon:local-dev /usr/src/app"
alias fbuild="build && dbuild"
alias clean="cd /usr/src/app && rm -rf ./build ./dist"
alias ad="python -m appdaemon"
'';
}
+4
View File
@@ -0,0 +1,4 @@
bash
cmake
git
tree
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -e
REPO_DIR=$(dirname $(readlink -f $BASH_SOURCE[0]))
alias compose="compose -f $REPO_DIR/docker-compose.yml"
cd $REPO_DIR
git submodule update --remote
docker compose pull --ignore-pull-failures
docker compose up -d --build --force-recreate
docker compose rm -f