diff --git a/.github/workflows/compile-all.yaml b/.github/workflows/compile-all.yaml new file mode 100644 index 0000000..e758519 --- /dev/null +++ b/.github/workflows/compile-all.yaml @@ -0,0 +1,25 @@ +name: Compile All + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Setup secrets + run: | + cp secrets.yaml.sample secrets.yaml + cp templates/secrets.yaml.sample templates/secrets.yaml + + - name: Compile All + run: | + nix develop --command make compile-all diff --git a/.gitignore b/.gitignore index 3ec88f7..7b66287 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,13 @@ secrets.yaml # direnv cache /.direnv + +# Make dependencies +.Makefile.deps + +# Nix build outputs +result +result-* + +# Pre-commit +.pre-commit-config.yaml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..59e8756 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# Automatically find all YAML files and exclude special/secrets files +ALL_YAML := $(wildcard *.yaml) +EXCLUDE_YAML := secrets.yaml btproxy.yaml +DEVICE_YAML := $(filter-out $(EXCLUDE_YAML), $(ALL_YAML)) +DEVICES := $(DEVICE_YAML:.yaml=) + +BTPROXY_REPLICAS := c25bac c2577c c25b74 + +STAMP_DIR := .esphome/stamps +MAKEFLAGS += --keep-going +FLASH_TARGETS := $(addprefix $(STAMP_DIR)/.stamp-flash-, $(DEVICES)) $(addprefix $(STAMP_DIR)/.stamp-flash-btproxy-, $(BTPROXY_REPLICAS)) +COMPILE_TARGETS := $(addprefix $(STAMP_DIR)/.stamp-compile-, $(DEVICES)) $(addprefix $(STAMP_DIR)/.stamp-compile-btproxy-, $(BTPROXY_REPLICAS)) + +ESPHOME_BIN := $(shell command -v esphome) +$(shell mkdir -p $(STAMP_DIR) && echo "$(ESPHOME_BIN)" > $(STAMP_DIR)/.esphome-bin.tmp && cmp -s $(STAMP_DIR)/.esphome-bin.tmp $(STAMP_DIR)/.esphome-bin || mv $(STAMP_DIR)/.esphome-bin.tmp $(STAMP_DIR)/.esphome-bin; rm -f $(STAMP_DIR)/.esphome-bin.tmp) +all: compile-all + +flash-all: $(FLASH_TARGETS) +compile-all: $(COMPILE_TARGETS) + +clean: + rm -rf $(STAMP_DIR) .Makefile.deps + +# Convenience targets +flash-%: $(STAMP_DIR)/.stamp-flash-% + @: +compile-%: $(STAMP_DIR)/.stamp-compile-% + @: + +# Normal devices: compile +$(STAMP_DIR)/.stamp-compile-%: %.yaml $(STAMP_DIR)/.esphome-bin + @mkdir -p $(STAMP_DIR) + esphome compile $< + @touch $@ + +# Normal devices: flash +$(STAMP_DIR)/.stamp-flash-%: %.yaml $(STAMP_DIR)/.esphome-bin + @mkdir -p $(STAMP_DIR) + esphome run --no-logs --device OTA $< + @touch $@ + +# Special btproxy devices: compile +$(STAMP_DIR)/.stamp-compile-btproxy-%: btproxy.yaml $(STAMP_DIR)/.esphome-bin + @mkdir -p $(STAMP_DIR) + esphome -s name btproxy-$* compile $< + @touch $@ + +# Special btproxy devices: flash +$(STAMP_DIR)/.stamp-flash-btproxy-%: btproxy.yaml $(STAMP_DIR)/.esphome-bin + @mkdir -p $(STAMP_DIR) + esphome -s name btproxy-$* run --no-logs --device OTA $< + @touch $@ + +# Include generated dependencies +-include .Makefile.deps + +# Rule to update dependencies +.Makefile.deps: $(wildcard *.yaml) $(wildcard templates/*.yaml) generate_deps.py + @echo "Updating dependencies..." + @python3 generate_deps.py "$(DEVICES)" "$(BTPROXY_REPLICAS)" > $@ + +.PHONY: all flash-all compile-all clean diff --git a/README.md b/README.md index 4052094..1104483 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,41 @@ timezone when syncing the clock (NTP). ## Compiling -``` +```bash $ cp -i secrets.yaml{.sample,} $ cp -i templates/secrets.yaml{.sample,} # Edit both secrets.yaml files. -$ ./reflash.sh compile '' +# Compile all devices in parallel (up to 4 jobs) +$ nix develop --command make compile-all -j4 + +# Or compile a single device +$ nix develop --command make compile-bathroomheater ``` -## Running +## Flashing -``` -$ ./reflash.sh +To flash all devices (automatically prioritizes OTA and skips prompts): + +```bash +$ nix develop --command make flash-all -j4 ``` -or +To flash a single device: -``` -$ esphome run file.yaml +```bash +$ nix develop --command make flash-bathroomheater ``` -Look up list of runnable configurations inside `reflash.sh` script. +Alternatively, you can drop into the development shell and run `esphome` directly: + +```bash +$ nix develop +$ esphome run bathroomheater.yaml +``` + +**Note:** The Makefile automatically tracks dependencies (including `!include` files and local `external_components` directories). It will natively evaluate changes and only recompile what has actually changed since the last run. ## Running remotely diff --git a/flake.lock b/flake.lock index 13ef078..4bc1189 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,72 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "fw_nix": { + "inputs": { + "git-hooks": "git-hooks", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1783867522, + "narHash": "sha256-rJUjsQbVFWWFQ7Xwt69Ad76v7OIpDXbqzW25d+I2BBs=", + "owner": "futureware-tech", + "repo": "nix", + "rev": "e132e29c641ee55aba9ad08507ace53e19cf2b3e", + "type": "github" + }, + "original": { + "owner": "futureware-tech", + "repo": "nix", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "fw_nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783008725, + "narHash": "sha256-jGiy6+sxjNWXSjp25uoJuNfyH9zBK1PEDY0lVoL4ibQ=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "bca82caa46d5ec0f5d422c61fb1e30bc51313cbe", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1783224372, - "narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=", + "lastModified": 1783776592, + "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d407951447dcd00442e97087bf374aad70c04cea", + "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", "type": "github" }, "original": { @@ -18,8 +78,24 @@ }, "root": { "inputs": { + "fw_nix": "fw_nix", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 8c1c934..c259512 100644 --- a/flake.nix +++ b/flake.nix @@ -3,29 +3,72 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + fw_nix = { + url = "github:futureware-tech/nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs }: + outputs = + { + self, + nixpkgs, + fw_nix, + }: let - forAllSystems = f: nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system: f { - pkgs = import nixpkgs { inherit system; }; - }); + forAllSystems = + f: + nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( + system: + f { + inherit system; + pkgs = import nixpkgs { inherit system; }; + } + ); in { - devShells = forAllSystems ({ pkgs }: { - default = pkgs.mkShell { - buildInputs = with pkgs; [ - esphome - esptool - cc2538-bsl - bash # for ./reflash.sh - ]; + checks = forAllSystems ( + { system, ... }: { + pre-commit-check = fw_nix.inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = fw_nix.lib.pre-commit.hooks // { + # Disable check-yaml because it fails on ESPHome's custom YAML tags (like !include, !secret) + check-yaml.enable = false; + }; + }; + } + ); - shellHook = '' - echo -n "ESPHome " - esphome --version - ''; - }; - }); + devShells = forAllSystems ( + { system, pkgs, ... }: + let + esphome-fhs = pkgs.buildFHSEnv { + name = "esphome"; + targetPkgs = + pkgs: with pkgs; [ + esphome + zlib + ]; + multiPkgs = pkgs: with pkgs; [ zlib ]; + runScript = "esphome"; + }; + in + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + esphome-fhs + esptool + cc2538-bsl + gnumake + ]; + + shellHook = '' + ${self.checks.${system}.pre-commit-check.shellHook} + echo -n "ESPHome " + esphome --version + ''; + }; + } + ); }; } diff --git a/generate_deps.py b/generate_deps.py new file mode 100755 index 0000000..c1ed4bf --- /dev/null +++ b/generate_deps.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import sys +import re +from pathlib import Path + + +def get_deps(file_path, visited=None): + if visited is None: + visited = set() + if str(file_path) in visited: + return set() + visited.add(str(file_path)) + + deps = set() + p = Path(file_path) + if not p.is_file(): + return deps + + with open(p, "r", encoding="utf-8") as f: + content = f.read() + + matches = re.findall(r"!include\s+([a-zA-Z0-9_/-]+\.yaml)", content) + for match in matches: + # Resolve path relative to the file being processed + if match.startswith("/"): + dep_path = Path(match) + else: + dep_path = (p.parent / match).resolve().relative_to(Path.cwd()) + + deps.add(str(dep_path)) + deps.update(get_deps(dep_path, visited)) + + # Also track local external_components directories + source_matches = re.findall(r"source:\s+([a-zA-Z0-9_/-]+)", content) + for match in source_matches: + if match.startswith("/"): + source_dir = Path(match) + else: + source_dir = (p.parent / match).resolve().relative_to(Path.cwd()) + + if source_dir.is_dir(): + for f in source_dir.rglob("*"): + if f.is_file(): + deps.add(str(f)) + + return deps + + +def main(): + devices = sys.argv[1].split() + btproxy_replicas = sys.argv[2].split() + + for device in devices: + yaml_file = f"{device}.yaml" + if not Path(yaml_file).is_file(): + continue + + deps = get_deps(yaml_file) + if deps: + deps_str = " ".join(sorted(deps)) + print(f"$(STAMP_DIR)/.stamp-compile-{device}: {deps_str}") + print(f"$(STAMP_DIR)/.stamp-flash-{device}: {deps_str}") + + if Path("btproxy.yaml").is_file(): + deps = get_deps("btproxy.yaml") + if deps: + deps_str = " ".join(sorted(deps)) + for replica in btproxy_replicas: + print(f"$(STAMP_DIR)/.stamp-compile-btproxy-{replica}: {deps_str}") + print(f"$(STAMP_DIR)/.stamp-flash-btproxy-{replica}: {deps_str}") + + +if __name__ == "__main__": + main() diff --git a/reflash.sh b/reflash.sh deleted file mode 100755 index 123c31d..0000000 --- a/reflash.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# Build a full list of known esphome devices and -# - reflash them -# - reflash those that match $1 -# - do $1 to those that match $2 -# -# Ex.: ./reflash.sh logs car - -set -e - -if [ $# -gt 1 ]; then - command=$1 - shift -else - command="run --no-logs" -fi - -declare -a flash - -for config in \ - bathroomheater.yaml \ - blinds.yaml \ - heating.yaml \ - loud-siren.yaml \ - watermeter.yaml \ - heating-eg.yaml \ - heating-og.yaml \ - car-charger.yaml \ - freezer.yaml \ - living-room-entertainment.yaml \ - living-room-infra.yaml \ - outdoor-equipment.yaml \ - storage-heater.yaml \ - basement-dehum.yaml \ - zigbee-coordinator-ch24.yaml \ - zigbee-coordinator-ch25.yaml \ - child-west-ac.yaml \ - child-east-ac.yaml \ - spare-power-meter.yaml \ - office-desk-north.yaml \ - shadow-udmp.yaml \ - attic-central.yaml \ - office-rack.yaml \ - radiation.yaml \ - cat-feeder-cam.yaml \ - btproxy-laundry.yaml \ - garage-gate.yaml \ - irrigation.yaml \ - tools-power.yaml; do - flash+=("${command?} ${config?}") -done - -if [[ "$command" == run* ]] || [[ "$command" == upload* ]] || [[ "$command" == logs* ]] || [[ "$command" == log* ]]; then - for btproxy_replica in \ - c25bac \ - c2577c \ - c25b74 - do - flash+=("${command?} btproxy.yaml --device btproxy-${btproxy_replica?}") - done -else - flash+=("${command?} btproxy.yaml") -fi - -declare -A flash_result - -print_results_table() { - printf -- "-%.0s" {1..80} - printf "\n%-70sSTATUS\n\n" "COMMAND" - for result_cmdline in "${flash[@]}"; do - printf "%-70s" "$result_cmdline " - if [[ -v flash_result["$result_cmdline"] ]]; then - echo "${flash_result["$result_cmdline"]}" - else - echo "N/A" - fi - done - printf "\n%-70s%d\n" "TOTAL" "${#flash[@]}" - printf -- "-%.0s" {1..80} - echo - echo -} - -for cmdline in "${flash[@]}"; do - if [ $# -gt 0 ]; then - if [[ "$cmdline" =~ $1 ]]; then - :; - else - flash_result["$cmdline"]=SKIPPED - continue - fi - fi - flash_result["$cmdline"]=CURRENT - - print_results_table - echo "Launching esphome: $cmdline" - if echo 1 | esphome $cmdline; then - flash_result["$cmdline"]=OK - else - flash_result["$cmdline"]=FAIL - fi -done - -print_results_table