Compare commits
174 Commits
main
..
6970bab14e
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.10
|
||||||
|
|
||||||
|
# install order matters because of some weird dependency stuff with websocket-client
|
||||||
|
# install appdaemon first because it's versioning is more restrictive
|
||||||
|
RUN pip install git+https://github.com/AppDaemon/appdaemon@dev
|
||||||
|
|
||||||
|
ENV CONF=/conf
|
||||||
|
RUN mkdir $CONF
|
||||||
|
COPY ./requirements.txt ${CONF}
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip pip install -r ${CONF}/requirements.txt
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
||||||
|
{
|
||||||
|
"name": "Appdaemon",
|
||||||
|
"build": {
|
||||||
|
// Sets the run context to one level up instead of the .devcontainer folder.
|
||||||
|
"context": "..",
|
||||||
|
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
|
||||||
|
"dockerfile": "../.devcontainer/Dockerfile"
|
||||||
|
},
|
||||||
|
|
||||||
|
"mounts": [
|
||||||
|
"source=/etc/localtime,target=/etc/localtime,type=bind,consistency=cached",
|
||||||
|
"source=/etc/timezone,target=/etc/timezone,type=bind,consistency=cached",
|
||||||
|
"source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind,consistency=cached"
|
||||||
|
],
|
||||||
|
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/conf,type=bind",
|
||||||
|
"workspaceFolder": "/conf",
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Uncomment the next line to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "cat /etc/os-release",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-toolsai.jupyter",
|
||||||
|
"mhutchie.git-graph",
|
||||||
|
"ms-python.isort",
|
||||||
|
"ms-python.autopep8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "devcontainer"
|
||||||
|
}
|
||||||
+12
-4
@@ -1,5 +1,13 @@
|
|||||||
# git.nix
|
__pycache__
|
||||||
*.env
|
*.pyc
|
||||||
.devenv
|
*.png
|
||||||
|
|
||||||
*.log
|
secrets.yaml
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
*.ipynb
|
||||||
|
|
||||||
|
logs/
|
||||||
|
*.json
|
||||||
|
|
||||||
|
namespaces/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[submodule "apps/room_control"]
|
||||||
|
path = apps/room_control
|
||||||
|
url = ssh://gitea/john/room_control
|
||||||
|
branch = main
|
||||||
|
[submodule "docker-observation"]
|
||||||
|
path = docker-observation
|
||||||
|
url = ssh://gitea/john/docker-observation
|
||||||
|
branch = main
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
keys:
|
|
||||||
- &1password age197d424aa7jpj2s735fl2h2s4c687y8vm44usx8wag0r2kh2v7ces4efdyn
|
|
||||||
- &lola-ad age1qf4c4asf7wcqqyd9aju8fq9dvum4ptcqr8dd6xqengsf6jx7daqqtgup30
|
|
||||||
creation_rules:
|
|
||||||
- path_regex: yaml$
|
|
||||||
key_groups:
|
|
||||||
- age:
|
|
||||||
- *1password
|
|
||||||
- *lola-ad
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
@@ -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://127.0.0.1:5050
|
||||||
|
|
||||||
|
logs:
|
||||||
|
main_log:
|
||||||
|
date_format: '%Y-%m-%d %I:%M:%S %p'
|
||||||
|
error_log:
|
||||||
|
date_format: '%Y-%m-%d %I:%M:%S %p'
|
||||||
@@ -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,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,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,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 2180e544c0
@@ -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, cb_args):
|
||||||
|
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
+130
@@ -0,0 +1,130 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from appdaemon.entity import Entity
|
||||||
|
from appdaemon.plugins.hass.hassapi import Hass
|
||||||
|
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
||||||
|
|
||||||
|
|
||||||
|
class SleepSetter(Hass, Mqtt):
|
||||||
|
def initialize(self):
|
||||||
|
assert self.entity_exists(entity_id=self.variable), f'{self.variable} does not exist'
|
||||||
|
self.listen_state(callback=self.handle_state, entity_id=self.variable)
|
||||||
|
self.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
+23
@@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
@@ -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
|
||||||
+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,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" ];
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
Submodule
+1
Submodule docker-observation added at 1ee44b7b80
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
|
||||||
Generated
-397
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
'';
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./home-manager.nix
|
|
||||||
./docker
|
|
||||||
./services
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./portainer.nix
|
|
||||||
./watchtower.nix
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -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,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 * * *";};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./promtail.nix
|
|
||||||
./telegraf.nix
|
|
||||||
./restic.nix
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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,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")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -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)"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user