Compare commits
184 Commits
flakes
..
4ff1ac573f
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ff1ac573f | |||
| 6fd891b0da | |||
| 28f2c4d094 | |||
| ea5a9542e1 | |||
| 3781a36be6 | |||
| 92b100e0ce | |||
| 2c89a044d4 | |||
| 96e22ac46d | |||
| e933e1c2b5 | |||
| 72b91c1c76 | |||
| 6970bab14e | |||
| 9e01dc7958 | |||
| fca47b8a60 | |||
| 4114fb4c54 | |||
| a8e0b53c53 | |||
| de31b6ca5a | |||
| 3bd955299c | |||
| a73153298a | |||
| 447f8bd354 | |||
| 689a9d0211 | |||
| dbdc088ca0 | |||
| 4014c60148 | |||
| 2355aadab7 | |||
| 8bbe64a9ec | |||
| bc175f75d9 | |||
| 5b56c29999 | |||
| 94fda2f84d | |||
| 8eb9e84b9d | |||
| a38341484d | |||
| aedee1ac12 | |||
| c4c4364225 | |||
| 4bf09a677b | |||
| 38e1cdaae2 | |||
| 7ee2b14008 | |||
| 01af9e106c | |||
| f64605b831 | |||
| da7e2b2d50 | |||
| 1fc7e45d26 | |||
| 055cf4e51c | |||
| b6223a95d2 | |||
| a79d441668 | |||
| 2c77c3aa66 | |||
| d7312bb3ef | |||
| 5db2976ce6 | |||
| 8383ff5cf1 | |||
| 8a84031813 | |||
| 522c41e733 | |||
| 88d1badf53 | |||
| 489076f011 | |||
| 88dbd0fd3f | |||
| af8d13bfce | |||
| dee31fe6f1 | |||
| 2a66ca4521 | |||
| 0eb484f187 | |||
| 28d0541284 | |||
| 986996c595 | |||
| 27c89cf542 | |||
| 4ab88d11ca | |||
| e71b003099 | |||
| 2cd374a927 | |||
| 496664e106 | |||
| b93b308131 | |||
| e3735e381b | |||
| 56e4d893f5 | |||
| 7456174ef6 | |||
| 80392fbf95 | |||
| 8d303438ce | |||
| 4b0b7c5e61 | |||
| 0102321b87 | |||
| f7d8fba8fb | |||
| 07330e442d | |||
| 00a7fd5048 | |||
| 9d0b5e4331 | |||
| 3b0b4e07bb | |||
| f877cab66d | |||
| e644eefa6d | |||
| 021dc61c6b | |||
| daf97fcb06 | |||
| 0b96430f7a | |||
| 89576840a5 | |||
| d34dfd2e56 | |||
| 117c7b3471 | |||
| 648bbc2a5b | |||
| 366e048456 | |||
| b4a6d9ac78 | |||
| 380be7d4a6 | |||
| 35f60f7b1a | |||
| 79beb97945 | |||
| 1ce2e73f77 | |||
| 525b519433 | |||
| d5e1cb174d | |||
| 1f30147a71 | |||
| 1fbe4d3dae | |||
| 964b7fa0b0 | |||
| f4c73c5ef7 | |||
| d57032bfb5 | |||
| 0deb3eb8f6 | |||
| 9597cf5942 | |||
| 0859a571ea | |||
| 5c20718f87 | |||
| 10d19e99d1 | |||
| d72ebbdeb4 | |||
| 08f3f95207 | |||
| b66879703f | |||
| 173f89eea5 | |||
| 7b859b682d | |||
| 1be85ae225 | |||
| 8917c8dbd7 | |||
| 6359ac9105 | |||
| 688df0185f | |||
| 80b7c50216 | |||
| d31298fa9f | |||
| 6326c95748 | |||
| 9a77854f85 | |||
| 82d8136e97 | |||
| c119765a38 | |||
| bbc3e94a7e | |||
| 162bd80409 | |||
| 7a08bd4f8e | |||
| 88c28248fd | |||
| c80b665f95 | |||
| 7f1083cc5d | |||
| 73d2ea117e | |||
| c621b8d31b | |||
| 61ae9f1149 | |||
| 46c3e8f951 | |||
| aa3cf277e7 | |||
| 7407a39bbb | |||
| 243e6de04f | |||
| 2ee2155265 | |||
| 0850828bda | |||
| a9f37c5e03 | |||
| 206f943e1a | |||
| 8880a409ff | |||
| e4f2a1f625 | |||
| 700c8e1214 | |||
| cd22364e21 | |||
| 32bc512d37 | |||
| aa5c0d0092 | |||
| ad2eda75cc | |||
| 5c8fa5e586 | |||
| 1aafa1dd11 | |||
| 97df288ff1 | |||
| 0088021b81 | |||
| 6fd87f1910 | |||
| c64a1f1a0a | |||
| 02c7cf98ee | |||
| f10927ccce | |||
| 34707cf517 | |||
| 97a6da6815 | |||
| cf528c5252 | |||
| 51534fc3ed | |||
| 92fa257829 | |||
| b355fb9b4f | |||
| 3dade9ffb5 | |||
| e5d3b31e4b | |||
| 35a4a3fd0e | |||
| f953d01094 | |||
| 798d837660 | |||
| 29e89e42b9 | |||
| d6bfcc864b | |||
| 1452f53f0a | |||
| efb30e3ddc | |||
| f2482b872a | |||
| c5cc1df425 | |||
| e301c6838f | |||
| 6aa9154a78 | |||
| e13fadc5cf | |||
| d32734cc92 | |||
| 6f93b9c364 | |||
| a257d5bde4 | |||
| 848bed6f88 | |||
| 40eeb81c1f | |||
| 9959545601 | |||
| ae8c4d4ef9 | |||
| dce065a996 | |||
| 22ad5180a3 | |||
| 164c7f5bbb | |||
| b4624fa97a | |||
| 6abca10479 | |||
| 0c409eabb0 | |||
| df2a19c2e6 | |||
| 7ff9b75c88 | |||
| 6c7b769628 |
+13
-2
@@ -1,2 +1,13 @@
|
||||
git.nix
|
||||
*.env
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.png
|
||||
|
||||
secrets.yaml
|
||||
.python-version
|
||||
|
||||
*.ipynb
|
||||
|
||||
logs/
|
||||
*.json
|
||||
|
||||
namespaces/
|
||||
@@ -0,0 +1,4 @@
|
||||
[submodule "apps/room_control"]
|
||||
path = apps/room_control
|
||||
url = ssh://gitea/john/room_control
|
||||
branch = main
|
||||
@@ -1,15 +1,16 @@
|
||||
NixOS Configuration for AppDaemon Development
|
||||
# AppDaemon
|
||||
|
||||
Needs a `git.nix` file. Example below:
|
||||
## Apps
|
||||
|
||||
```shell
|
||||
{ ... }:
|
||||
{
|
||||
programs.git = {
|
||||
enable = true;
|
||||
extraConfig.credential.helper = "store --file ~/.git-credentials";
|
||||
userName = "John Lancaster";
|
||||
userEmail = "32917998+jsl12@users.noreply.github.com";
|
||||
};
|
||||
}
|
||||
```
|
||||
### Room Control
|
||||
|
||||
- living_room
|
||||
- kitchen
|
||||
- bedroom
|
||||
- bathroom
|
||||
|
||||
### Cube
|
||||
|
||||
### Sleep Setter
|
||||
|
||||
### TV
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "/usr/src/app"
|
||||
},
|
||||
{
|
||||
"path": "/conf"
|
||||
},
|
||||
{
|
||||
"path": "/srv/appdaemon/snippets"
|
||||
},
|
||||
{
|
||||
"path": "/srv/appdaemon/ad-nix"
|
||||
},
|
||||
],
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/src/app/.venv/bin/python3"
|
||||
}
|
||||
}
|
||||
Executable
+40
@@ -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'
|
||||
Generated
-301
@@ -1,301 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"git-hooks": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728672398,
|
||||
"narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "aac51f698309fd0f381149214b7eee213c66ef0a",
|
||||
"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_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733323168,
|
||||
"narHash": "sha256-d5DwB4MZvlaQpN6OQ4SLYxb5jA4UH5EtV5t5WOtjLPU=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "efa9010b8b1cfd5dd3c7ed1e172a470c3b84a064",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712014858,
|
||||
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1730302582,
|
||||
"narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"libgit2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1697646580,
|
||||
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-parts": "flake-parts",
|
||||
"libgit2": "libgit2",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-23-11": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs-regression": [
|
||||
"devenv"
|
||||
],
|
||||
"pre-commit-hooks": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1727438425,
|
||||
"narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.24",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1730531603,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-python": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733319315,
|
||||
"narHash": "sha256-cFQBdRmtIZFVjr2P6NkaCOp7dddF93BC0CXBwFZFaN0=",
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"rev": "01263eeb28c09f143d59cd6b0b7c4cc8478efd48",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1717432640,
|
||||
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1716977621,
|
||||
"narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1733212471,
|
||||
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"nixpkgs-python": "nixpkgs-python"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
devenv.url = "github:cachix/devenv";
|
||||
nixpkgs-python = {
|
||||
url = "github:cachix/nixpkgs-python";
|
||||
inputs = { nixpkgs.follows = "nixpkgs"; };
|
||||
};
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
|
||||
extra-substituters = "https://devenv.cachix.org";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, devenv, ... } @ inputs:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
adPath = "/usr/src/app";
|
||||
in
|
||||
{
|
||||
packages.${system} = {
|
||||
devenv-up = self.devShells.${system}.default.config.procfileScript;
|
||||
devenv-test = self.devShells.${system}.default.config.test;
|
||||
};
|
||||
|
||||
devShells.${system}.default = devenv.lib.mkShell {
|
||||
inherit inputs pkgs;
|
||||
modules = [
|
||||
({ pkgs, config, ... }: {
|
||||
# This is your devenv configuration
|
||||
|
||||
pre-commit.hooks = {
|
||||
end-of-file-fixer.enable = true;
|
||||
trim-trailing-whitespace.enable = true;
|
||||
};
|
||||
|
||||
languages.python = {
|
||||
enable = true;
|
||||
version = "3.12.7";
|
||||
uv = {
|
||||
enable = true;
|
||||
sync = {
|
||||
enable = true;
|
||||
allExtras = true;
|
||||
arguments = [ "-U" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
packages = with pkgs; [
|
||||
git
|
||||
(writeShellScriptBin "full-build" ''
|
||||
cd ${adPath}
|
||||
${pkgs.uv}/bin/uv build --wheel
|
||||
docker build -t acockburn/appdaemon:local-dev ${adPath}
|
||||
'')
|
||||
];
|
||||
|
||||
enterShell = ''
|
||||
alias appdaemon="${pkgs.uv}/bin/uv run --frozen python -m appdaemon"
|
||||
alias ad="appdaemon"
|
||||
|
||||
export PS1="\[\e[0;34m\](AppDaemon)\[\e[0m\] ''${PS1-}"
|
||||
|
||||
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: \e[32m$(${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"
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
Executable
+113
@@ -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')
|
||||
@@ -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')
|
||||
@@ -0,0 +1,9 @@
|
||||
cube1:
|
||||
module: cube
|
||||
class: AqaraCube
|
||||
rich: DEBUG
|
||||
cube: Cube 1
|
||||
app: bedroom
|
||||
flip90: toggle
|
||||
flip180: scene.bedsport
|
||||
# shake: activate
|
||||
Executable
+186
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
weather:
|
||||
module: weather
|
||||
class: Weather
|
||||
# log_level: DEBUG
|
||||
location: 78704 US
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
HelloWorld:
|
||||
module: hello
|
||||
class: HelloWorld
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
# )
|
||||
Submodule
+1
Submodule apps/room_control added at e3186f1b5e
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+42
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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}')
|
||||
Executable
+30
@@ -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
|
||||
@@ -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'],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
traffic:
|
||||
module: traffic
|
||||
class: TrafficAlert
|
||||
+63
@@ -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')
|
||||
@@ -1,94 +0,0 @@
|
||||
{ pkgs, lib, modulesPath, ... }:
|
||||
let
|
||||
stateVersion = "24.05";
|
||||
unstable = import <nixos-unstable> {};
|
||||
adHome = "/srv/appdaemon";
|
||||
adNixPath = "${adHome}/ad-nix";
|
||||
adPath = "/usr/src/app";
|
||||
adRepo = "https://github.com/jsl12/appdaemon";
|
||||
adBranch = "hass";
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/virtualisation/proxmox-lxc.nix")
|
||||
(import "${builtins.fetchTarball https://github.com/nix-community/home-manager/archive/release-24.05.tar.gz}/nixos")
|
||||
(fetchTarball "https://github.com/nix-community/nixos-vscode-server/tarball/master")
|
||||
./telegraf.nix
|
||||
./promtail.nix
|
||||
./portainer.nix
|
||||
./watchtower.nix
|
||||
];
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
(pkgs.writeShellScriptBin "nrbs" "sudo nixos-rebuild switch")
|
||||
(pkgs.writeShellScriptBin "nrbsu" "sudo nix-channel --update && sudo nixos-rebuild switch")
|
||||
(pkgs.writeShellScriptBin "ads" ''
|
||||
cd ${adPath}
|
||||
nix develop --no-pure-eval ${adNixPath}/appdaemon
|
||||
'')
|
||||
(pkgs.writeShellScriptBin "ad-clone" ''
|
||||
if [ ! -d ${adPath} ]; then
|
||||
sudo git clone -b ${adBranch} ${adRepo} ${adPath}
|
||||
sudo chown -R appdaemon:users $(dirname ${adPath})
|
||||
else
|
||||
echo "${adPath} already exists"
|
||||
fi
|
||||
'')
|
||||
# unstable.uv
|
||||
bash
|
||||
git
|
||||
eza
|
||||
gh
|
||||
# appdaemon
|
||||
];
|
||||
|
||||
time.timeZone = "America/Chicago";
|
||||
|
||||
virtualisation.docker.enable = true;
|
||||
virtualisation.oci-containers.backend = "docker";
|
||||
|
||||
services.vscode-server.enable = true;
|
||||
services.openssh.enable = true;
|
||||
services.tailscale.enable = true;
|
||||
|
||||
system.activationScripts.ensureDirectory = ''
|
||||
if [ ! -d /conf ]; then
|
||||
mkdir /conf
|
||||
chmod 0755 /conf
|
||||
chown 1000:100 /conf
|
||||
fi
|
||||
'';
|
||||
|
||||
security.sudo-rs = {
|
||||
enable = true;
|
||||
execWheelOnly = false;
|
||||
wheelNeedsPassword = false;
|
||||
};
|
||||
|
||||
users.users.appdaemon = {
|
||||
isNormalUser = true;
|
||||
home = "${adHome}";
|
||||
extraGroups = [ "wheel" "docker" ];
|
||||
openssh.authorizedKeys.keyFiles = [ "/root/.ssh/authorized_keys" ];
|
||||
};
|
||||
|
||||
nix.settings.trusted-users = [ "root" "@wheel" ];
|
||||
|
||||
home-manager = {
|
||||
useGlobalPkgs = true;
|
||||
users.appdaemon = { pkgs, ... }: {
|
||||
home.stateVersion = stateVersion;
|
||||
imports = [ ./git.nix ];
|
||||
programs = {
|
||||
ssh.enable = true;
|
||||
git.extraConfig.safe.directory = "${adNixPath}";
|
||||
bash = {
|
||||
enable = true;
|
||||
profileExtra = "cd ${adNixPath}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
system.stateVersion = stateVersion;
|
||||
}
|
||||
@@ -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
|
||||
Executable
+6
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{ config, ... }:
|
||||
let
|
||||
lokiHost = "192.168.1.174:3100";
|
||||
in
|
||||
{
|
||||
systemd.services.promtail.serviceConfig = {
|
||||
SupplementaryGroups = [ "docker" ];
|
||||
};
|
||||
|
||||
services.promtail = {
|
||||
enable = true;
|
||||
configuration = {
|
||||
server = {
|
||||
http_listen_port = 3031;
|
||||
grpc_listen_port = 0;
|
||||
};
|
||||
positions = {
|
||||
filename = "/tmp/positions.yaml";
|
||||
};
|
||||
clients = [{url = "http://${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
|
||||
};
|
||||
}
|
||||
Executable
+8
@@ -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
|
||||
@@ -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"
|
||||
'';
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
bash
|
||||
cmake
|
||||
git
|
||||
tree
|
||||
@@ -1,51 +0,0 @@
|
||||
{ ... }:
|
||||
let
|
||||
influxURL = "http://panoptes.john-stream.com:8086";
|
||||
organization = "homelab";
|
||||
bucket = "docker";
|
||||
envFile = ./telegraf.env;
|
||||
in
|
||||
{
|
||||
systemd.services.telegraf.serviceConfig = {
|
||||
SupplementaryGroups = [ "docker" ];
|
||||
};
|
||||
|
||||
services.telegraf = {
|
||||
enable = true;
|
||||
environmentFiles = [ "${envFile}" ];
|
||||
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"];
|
||||
total = false;
|
||||
docker_label_include = [];
|
||||
};
|
||||
};
|
||||
outputs = {
|
||||
influxdb_v2 = {
|
||||
urls = ["${influxURL}"];
|
||||
token = "$INFLUX_WRITE_TOKEN";
|
||||
organization = "${organization}";
|
||||
bucket = "${bucket}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 * * *";};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user