diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a298f9f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,47 @@ + +Version: + + +## What does this implement/fix? + + + +## Types of changes + + +- [ ] Bugfix (fixed change that fixes an issue) +- [ ] New feature (thanks!) +- [ ] Breaking change (repair/feature that breaks existing functionality) +- [ ] Dependency Update - Does not publish +- [ ] Other - Does not publish +- [ ] Website of github readme file update - Does not publish +- [ ] Github workflows - Does not publish + + +## Checklist / Checklijst: + + + - [ ] The code change has been tested and works locally + - [ ] The code change has not yet been tested + +If user-visible functionality or configuration variables are added/modified: + - [ ] Added/updated documentation for the web page + + \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..7d579fe --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,51 @@ +name-template: 'Release v$NEXT_PATCH_VERSION' +tag-template: "$RESOLVED_VERSION" +change-template: "- #$NUMBER $TITLE @$AUTHOR" +sort-direction: ascending + +categories: + - title: "🚨 Breaking changes" + labels: + - "breaking-change" + - title: "✨ New features" + labels: + - "new-feature" + - title: "🐛 Bug fixes" + labels: + - "bugfix" +# - title: "🚀 Enhancements" +# labels: +# - "enhancement" +# - "refactor" +# - "performance" +# - title: "🧰 Maintenance" +# labels: +# - "maintenance" +# - "ci" +# - title: "📚 Documentation" +# labels: +# - "documentation" + - title: "⬆️ Dependency updates" + collapse-after: 5 + labels: + - "dependency-update" +# - title: "🚨🚨 Security Fixes 🚨🚨" +# labels: +# - "security" + + +include-labels: + - "bugfix" + - "new-feature" + - "breaking-change" + +no-changes-template: '- No changes' + +template: | + ## What's Changed + + $CHANGES + + **Full Changelog**: https://github.com/ApolloAutomation/H-3/compare/$PREVIOUS_TAG...$RESOLVED_VERSION.1 + + Be sure to 🌟 this repository for updates! \ No newline at end of file diff --git a/.github/workflows/autoassign.yml b/.github/workflows/autoassign.yml index a70c3da..03b36f5 100644 --- a/.github/workflows/autoassign.yml +++ b/.github/workflows/autoassign.yml @@ -1,9 +1,12 @@ name: Auto Assign +# pull_request_target (not pull_request) so auto-assign works on +# fork-submitted PRs; fork pull_request runs only get a read-only token. +# Safe because this workflow never checks out or executes PR code. on: issues: types: [opened] - pull_request: + pull_request_target: types: [opened] permissions: @@ -12,9 +15,7 @@ permissions: jobs: auto-assign: - # Skip auto-assign for pull requests from forks due to GITHUB_TOKEN permission restrictions - if: github.event_name == 'issues' || github.event.pull_request.head.repo.full_name == github.repository uses: ApolloAutomation/Workflows/.github/workflows/autoassign.yml@main with: - assignees: TrevorSchirmer + assignees: bharvey88 num-of-assignees: 1 diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml new file mode 100644 index 0000000..4498b12 --- /dev/null +++ b/.github/workflows/build-beta.yml @@ -0,0 +1,120 @@ +name: Build and Publish Beta + +# Builds H-3 firmware from the beta branch and publishes it as assets on a +# rolling "beta-fw" pre-release. The on-device "Firmware Channel" select points +# OTA updates at these assets. Stable firmware is built/published separately +# by build.yml (push to main -> GitHub Pages). + +on: + push: + branches: [beta] + paths: + - 'Integrations/ESPHome/**' + workflow_dispatch: + +# Least privilege: read-only by default; only publish-beta is elevated to write. +permissions: + contents: read + +jobs: + version: + name: Read version + runs-on: ubuntu-latest + outputs: + v: ${{ steps.read.outputs.v }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - id: read + run: | + v=$(awk '/substitutions:/ {f=1} f && /version:/ {print $2; exit}' \ + Integrations/ESPHome/Core.yaml | tr -d '"') + echo "v=$v" >> "$GITHUB_OUTPUT" + echo "Beta version: $v" + + build: + name: Build ${{ matrix.name }} + needs: version + strategy: + matrix: + include: + # Beta serves OTA updates only, so it builds the end-user image + # (H-3.yaml), not the first-flash Factory image. + - { yaml: Integrations/ESPHome/H-3.yaml, name: firmware-standard } + uses: esphome/workflows/.github/workflows/build.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 + with: + files: ${{ matrix.yaml }} + esphome-version: stable + combined-name: ${{ matrix.name }} + release-version: ${{ needs.version.outputs.v }} + + publish-beta: + name: Publish beta release assets + needs: [version, build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download firmware artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + # Downloaded by name, not pattern: with a single matching artifact + # the action extracts flat into `path` instead of creating a + # per-artifact subdirectory. H-3 builds one beta image (H-3D has no + # OTA), so `pattern:` would land the files in fw/ and break the + # publish step below. `name:` pins the layout regardless of count. + name: firmware-standard + path: fw/firmware-standard + + - name: Ensure rolling 'beta-fw' pre-release exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view beta-fw -R "${{ github.repository }}" >/dev/null 2>&1 \ + || gh release create beta-fw -R "${{ github.repository }}" \ + --prerelease --title "Beta (rolling)" \ + --notes "Latest H-3 beta firmware. Auto-updated on every push to the beta branch." + + - name: Rewrite manifests to absolute URLs and upload assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" + declare -A DIRS=( [standard]=firmware-standard ) + for v in standard; do + src="fw/${DIRS[$v]}" + man=$(find "$src" -name manifest.json | head -1) + if [ -z "$man" ]; then + echo "::error::manifest.json not found for ${DIRS[$v]}" + exit 1 + fi + # Release assets are a flat namespace: prefix per variant, same + # naming as the rest of the fleet. + find "$src" -name '*.bin' | while read -r bin; do + mv "$bin" "$(dirname "$bin")/$v-$(basename "$bin")" + done + echo "Rewriting $man" + # Make ota.path and parts[].path absolute release-asset URLs so the + # device never has to resolve a relative path against a redirect. + jq --arg base "$BASE" --arg pfx "$v-" ' + .builds[0].ota.path = ($base + "/" + $pfx + (.builds[0].ota.path | sub(".*/"; ""))) + | .builds[0].parts |= map(.path = ($base + "/" + $pfx + (.path | sub(".*/"; "")))) + ' "$man" > "manifest-$v.json" + cat "manifest-$v.json" + gh release upload beta-fw "manifest-$v.json" -R "${{ github.repository }}" --clobber + find "$src" -name '*.bin' -print -exec \ + gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; + done + echo "Beta assets published." + + - name: Point beta-fw tag at the built commit + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # gh release create tags default-branch HEAD, and uploads never move + # the tag, so without this the release's source commit drifts away + # from the assets actually published. + gh api -X PATCH "repos/${{ github.repository }}/git/refs/tags/beta-fw" \ + -f sha="${{ github.sha }}" -F force=true + echo "beta-fw -> ${{ github.sha }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ac0084..50aafd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,11 @@ on: pull_request: permissions: - pull-requests: write - issues: write contents: read +# Label check lives in label-check.yml on pull_request_target so it can +# label fork-submitted PRs; plain pull_request tokens are read-only there. jobs: - label-check: - name: Label Check - uses: ApolloAutomation/Workflows/.github/workflows/label-check.yml@main - ci: name: CI uses: ApolloAutomation/Workflows/.github/workflows/esphome-ci.yml@main @@ -20,3 +16,4 @@ jobs: yaml-files: | Integrations/ESPHome/H-3.yaml Integrations/ESPHome/H-3D.yaml + diff --git a/.github/workflows/label-check.yml b/.github/workflows/label-check.yml new file mode 100644 index 0000000..96a9751 --- /dev/null +++ b/.github/workflows/label-check.yml @@ -0,0 +1,20 @@ +name: Label Check + +# pull_request_target (not pull_request) so the job gets a write token on +# fork-submitted PRs too; plain pull_request runs from forks are read-only +# and cannot add labels. Safe because the called workflow only reads the PR +# body and never checks out or executes PR code. The "edited" type re-runs +# the check when the template checkboxes are changed. +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +permissions: + pull-requests: write + issues: write + contents: read + +jobs: + label-check: + name: Label Check + uses: ApolloAutomation/Workflows/.github/workflows/label-check.yml@main diff --git a/H-3.jpg b/H-3.jpg new file mode 100644 index 0000000..af88a24 Binary files /dev/null and b/H-3.jpg differ diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 527cfe6..553b93a 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,6 @@ substitutions: - version: "26.8.14.1" + version: "26.8.23.1" + run_duration_default: "90s" run_duration_gpio: "90s" timer_wake_enabled: "true" diff --git a/Integrations/ESPHome/H-3.yaml b/Integrations/ESPHome/H-3.yaml index ff8caa8..43635c4 100644 --- a/Integrations/ESPHome/H-3.yaml +++ b/Integrations/ESPHome/H-3.yaml @@ -1,3 +1,11 @@ +substitutions: + # Manifest URL bases. Stable = GitHub Pages (main branch builds). + # Beta = rolling "beta-fw" pre-release assets (beta branch builds). + stable_manifest_base: "https://apolloautomation.github.io/H-3" + beta_manifest_base: "https://github.com/ApolloAutomation/H-3/releases/download/beta-fw" + ota_stable_manifest: "${stable_manifest_base}/firmware/manifest.json" + ota_beta_manifest: "${beta_manifest_base}/manifest-standard.json" + esphome: name: "apollo-h-3" friendly_name: Apollo H-3 @@ -14,6 +22,10 @@ esphome: # safe_mode:); tracks the rest of the fleet rather than the true minimum. min_version: 2025.11.0 on_boot: + # Point the update entity at the selected channel manifest. + - priority: -100 + then: + - script.execute: apply_ota_source # H-3 is opt-in for deep sleep - priority: 800 then: @@ -49,6 +61,18 @@ esphome: - lambda: "return id(runTest);" then: - script.execute: testScript + # Re-apply the Bluetooth Proxy switch after all components set up, so BLE + # scanning matches the switch state restored from flash. + - priority: -300 + then: + - if: + condition: + switch.is_on: bluetooth_proxy_switch + then: + - esp32_ble_tracker.start_scan: + continuous: true + else: + - esp32_ble_tracker.stop_scan: on_shutdown: - light.turn_off: body_light - light.turn_off: mouth_light @@ -69,16 +93,31 @@ ota: - platform: http_request id: ota_managed +esp32_ble_tracker: + id: ble_tracker + scan_parameters: + continuous: true + +bluetooth_proxy: + http_request: verify_ssl: true + # GitHub release-asset downloads answer with a redirect carrying a + # ~3.6 KB Content-Security-Policy header; each header line must fit + # this buffer or the request fails with "HTTP_CLIENT: Out of buffer". + buffer_size_rx: 5120 + # The redirect target is a signed URL with a ~850-char query string; the + # follow-up request line must fit the TX buffer or esp_http_client_open + # fails with "Out of buffer" before sending anything. + buffer_size_tx: 2048 safe_mode: update: - platform: http_request - id: firmware_update + id: update_http_request name: Firmware Update - source: https://apolloautomation.github.io/H-3/firmware/manifest.json + source: ${ota_stable_manifest} wifi: power_save_mode: HIGH @@ -88,6 +127,8 @@ wifi: logger: +improv_serial: + captive_portal: web_server: @@ -130,7 +171,52 @@ api: - lambda: |- ESP_LOGW("Apollo", "Preventing Deep Sleep Due To OTA Or Switch"); id(deep_sleep_1).prevent_deep_sleep(); +select: + - platform: template + name: "Firmware Channel" + id: firmware_channel + icon: mdi:source-branch + entity_category: "config" + optimistic: true + restore_value: true + options: + - "Stable" + - "Beta" + initial_option: "Stable" + on_value: + then: + - script.execute: apply_ota_source + +button: + - platform: template + name: "Firmware Update" + id: update_firmware + icon: mdi:cloud-download + entity_category: "config" + on_press: + - logger.log: "Applying firmware update for the selected channel" + - delay: 3s + - script.execute: apply_ota_source + - script.wait: apply_ota_source + # The manifest fetch runs in its own task; give it a fixed window to land + # (update.is_available stays false for same-version switches). + - delay: 5s + - lambda: id(update_http_request).perform(true); + switch: + - platform: template + name: "Bluetooth Proxy" + id: bluetooth_proxy_switch + icon: mdi:bluetooth + entity_category: "config" + restore_mode: RESTORE_DEFAULT_OFF + optimistic: true + on_turn_on: + - esp32_ble_tracker.start_scan: + continuous: true + on_turn_off: + - esp32_ble_tracker.stop_scan: + - platform: template name: "Prevent Sleep" id: prevent_sleep @@ -180,6 +266,16 @@ sensor: entity_category: "diagnostic" script: + - id: apply_ota_source + # Sets the OTA manifest URL from the Firmware Channel select (Stable/Beta). + then: + - lambda: |- + const bool beta = id(firmware_channel).current_option() == "Beta"; + std::string url = beta ? "${ota_beta_manifest}" : "${ota_stable_manifest}"; + ESP_LOGI("firmware", "OTA manifest set to: %s", url.c_str()); + id(update_http_request).set_source_url(url); + - component.update: update_http_request + - id: statusCheck then: - if: diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 5f6a192..0000000 --- a/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -Version: - -Adds: - -Fixes: - -Breaks: - - - -Checks: -- [ ] Documentation Updated -- [ ] Build Number Incremented In Core.yaml diff --git a/README.md b/README.md index 83f39d4..5800e64 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ # Apollo H-3 Holiday Ornament +![Apollo Automation H-3](H-3.jpg) + **Key Features of the H-3:** -- USB-C or battery-powered ornament with 37 controllable RGB LEDs across five zones and a buzzer to play holiday tunes +- USB-C or battery powered ornament with 37 controllable RGB LEDs across five zones and a buzzer to play holiday tunes - Individually controllable body (24 LEDs), hat (5 LEDs), mouth (5 LEDs), eyes (2 LEDs), and nose (1 LED) - Comes preloaded with 5 holiday tunes and RGB LED patterns - Comes flashed with basic firmware but can be flashed with our smart firmware to connect and control from Home Assistant - Could also be used as a bluetooth proxy or bluetooth tracker - Features an ESP32-C6 processor with 8MB flash +**100% of profits donated to:** + +- **[CASA of Lexington](https://casaoflexington.org/)** - Advocates for children in the family court system + +- **[Open Home Foundation](https://www.openhomefoundation.org/)** - Apollo Automation is proud to back the Open Home Foundation. A portion of profits from every H-3 sold helps protect the values of privacy, choice, and sustainability in the smart home by supporting open source projects like Home Assistant and open connectivity standards. + ## Links Discord (Support/feedback/discussion/future products): [http://dsc.gg/ApolloAutomation](http://dsc.gg/ApolloAutomation) -Shop: [https://apolloautomation.com](https://apolloautomation.com) +Shop: [https://apolloautomation.com/products/apollo-h-3-annual-holiday-ornament-for-charity](https://apolloautomation.com/products/apollo-h-3-annual-holiday-ornament-for-charity) Wiki: [https://wiki.apolloautomation.com](https://wiki.apolloautomation.com)