├── .editorconfig ├── .env ├── .github ├── codeql │ └── codeql-config.yml ├── dependabot.yml └── workflows │ ├── bump-version.yml │ ├── check.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── cspell.config.yaml ├── eslint.config.mjs ├── fixtures.json ├── hass ├── README.md ├── compress.sh ├── decompress.sh ├── docker-compose.yaml └── reference.tar.gz ├── package.json ├── scripts └── mock-assistant.sh ├── src ├── dev │ ├── index.mts │ ├── mappings.mts │ ├── registry.mts │ └── services.mts ├── hass.module.mts ├── helpers │ ├── backup.mts │ ├── constants.mts │ ├── device.mts │ ├── entity-state.mts │ ├── features.mts │ ├── fetch.mts │ ├── fetch │ │ ├── calendar.mts │ │ ├── configuration.mts │ │ ├── index.mts │ │ ├── server-log.mts │ │ ├── service-list.mts │ │ └── weather-forecasts.mts │ ├── id-by.mts │ ├── index.mts │ ├── interfaces.mts │ ├── manifest.mts │ ├── notify.mts │ ├── registry.mts │ ├── utility.mts │ └── websocket.mts ├── index.mts ├── merge.mts ├── mock_assistant │ ├── helpers │ │ ├── fixtures.mts │ │ └── index.mts │ ├── index.mts │ ├── main.mts │ ├── mock-assistant.module.mts │ └── services │ │ ├── area.service.mts │ │ ├── config.service.mts │ │ ├── device.service.mts │ │ ├── entity-registry.service.mts │ │ ├── entity.service.mts │ │ ├── events.service.mts │ │ ├── fetch.service.mts │ │ ├── fixtures.service.mts │ │ ├── floor.service.mts │ │ ├── index.mts │ │ ├── label.service.mts │ │ ├── services.service.mts │ │ ├── websocket-api.service.mts │ │ └── zone.service.mts ├── quickboot.module.mts ├── services │ ├── area.service.mts │ ├── backup.service.mts │ ├── call-proxy.service.mts │ ├── config.service.mts │ ├── conversation.service.mts │ ├── device.service.mts │ ├── diagnostics.service.mts │ ├── entity.service.mts │ ├── events.service.mts │ ├── fetch-api.service.mts │ ├── floor.service.mts │ ├── id-by.service.mts │ ├── index.mts │ ├── internal.service.mts │ ├── label.service.mts │ ├── reference.service.mts │ ├── registry.service.mts │ ├── websocket-api.service.mts │ └── zone.service.mts ├── testing │ ├── area.spec.mts │ ├── backup.spec.mts │ ├── config.spec.mts │ ├── device.spec.mts │ ├── entity.spec.mts │ ├── events.spec.mts │ ├── fetch-api.spec.mts │ ├── fetch.spec.mts │ ├── fixtures.spec.mts │ ├── floor.spec.mts │ ├── id-by.spec.mts │ ├── label.spec.mts │ ├── ref-by.spec.mts │ ├── websocket.spec.mts │ ├── workflow.spec.mts │ └── zone.spec.mts └── user.mts ├── test-setup.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.spec.json ├── vitest.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | HASS_BASE_URL=http://localhost:9123 2 | HASS_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhYTdjOGQ5MzBiMDM0MDM2OGVjZTdjOTRjMTcyOWQ0OSIsImlhdCI6MTcxNDYwMzkyMywiZXhwIjoyMDI5OTYzOTIzfQ.Tvoh25bukQh5T5WIuvvkL9jVBihcOGG6D5JYdqdwx1U 3 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "DA CodeQL" 2 | version: 3 3 | options: 4 | # Adjust the number of threads used by CodeQL 5 | threads: 0 # Use all available CPU cores 6 | # Enable experimental analysis options if needed 7 | experimental: false 8 | queries: 9 | # Specify the query packs to use 10 | - name: security-and-quality 11 | include: 12 | - codeql/javascript-queries 13 | # Additional custom queries can be added here if needed 14 | databases: 15 | # Specify the languages and their corresponding extractors 16 | languages: 17 | javascript: 18 | # Specify paths for the TypeScript configuration file 19 | typescript: [tsconfig.json] 20 | include: 21 | - src/ 22 | # Paths to the source code to be analyzed 23 | source: 24 | - src/ 25 | # Paths to external dependencies to be excluded 26 | exclude: 27 | - node_modules/ 28 | - dist/ 29 | - out/ 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'monday' 8 | time: '09:00' 9 | timezone: 'America/New_York' 10 | target-branch: 'main' 11 | groups: 12 | minor-patch-dependencies: 13 | update-types: 14 | - 'minor' 15 | - 'patch' 16 | - package-ecosystem: 'github-actions' 17 | directory: '/' 18 | schedule: 19 | interval: 'weekly' 20 | day: 'monday' 21 | time: '09:00' 22 | timezone: 'America/New_York' 23 | target-branch: 'main' 24 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: Bump Package Version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_type: 7 | description: 'Release type' 8 | required: true 9 | default: 'prerelease' 10 | type: choice 11 | options: 12 | - release 13 | - prerelease 14 | 15 | jobs: 16 | bump-version: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | pull-requests: write 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v5 25 | with: 26 | token: ${{ secrets.DA_REPO_TOKEN }} 27 | fetch-depth: 0 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: '22' 31 | - run: corepack enable 32 | 33 | - run: yarn config set enableImmutableInstalls false 34 | - run: yarn 35 | 36 | - name: Get current version 37 | id: current_version 38 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 39 | 40 | - name: Bump version 41 | id: bump_version 42 | run: | 43 | if [ "${{ inputs.release_type }}" = "release" ]; then 44 | # Set to calver format (YY.MM.DD) 45 | CALVER_VERSION=$(date +%y.%-m.%-d) 46 | npm version $CALVER_VERSION --no-git-tag-version 47 | echo "new_version=$CALVER_VERSION" >> $GITHUB_OUTPUT 48 | echo "bump_type=calver" >> $GITHUB_OUTPUT 49 | else 50 | # Bump beta version using yarn 51 | npm version prerelease --preid beta --no-git-tag-version 52 | NEW_VERSION=$(node -p "require('./package.json').version") 53 | echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT 54 | echo "bump_type=beta" >> $GITHUB_OUTPUT 55 | fi 56 | 57 | - name: Create commit 58 | run: | 59 | git config --local user.email "action@github.com" 60 | git config --local user.name "GitHub Action" 61 | git add package.json 62 | git commit -m "chore: bump version to ${{ steps.bump_version.outputs.new_version }} (${{ steps.bump_version.outputs.bump_type }})" 63 | 64 | - name: Push changes 65 | run: git push origin HEAD:${{ github.ref }} 66 | 67 | - name: Create and push tag 68 | run: | 69 | git tag -a "v${{ steps.bump_version.outputs.new_version }}" -m "Release version ${{ steps.bump_version.outputs.new_version }}" 70 | git push origin "v${{ steps.bump_version.outputs.new_version }}" 71 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Checks 2 | 3 | permissions: 4 | contents: read 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | on: 11 | pull_request: 12 | branches: 13 | - main 14 | push: 15 | branches: 16 | - main 17 | 18 | jobs: 19 | lint-and-build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v5 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '22' 26 | - run: corepack enable 27 | - run: yarn config set enableImmutableInstalls false 28 | - run: yarn 29 | - run: yarn lint 30 | - run: yarn build 31 | - run: yarn test:coverage 32 | - name: Upload coverage reports to Codecov 33 | uses: codecov/codecov-action@v5.4.3 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published, prereleased] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | concurrency: 14 | group: publish-${{ github.event.release.tag_name }} 15 | cancel-in-progress: true 16 | environment: 17 | name: npm 18 | url: ${{ github.event.release.html_url }} 19 | steps: 20 | - uses: actions/checkout@v5 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: '22' 24 | registry-url: 'https://registry.npmjs.org/' 25 | - run: corepack enable 26 | - run: yarn config set enableImmutableInstalls false 27 | 28 | - run: yarn 29 | - run: yarn build 30 | - run: yarn lint 31 | - run: yarn test src/testing 32 | - name: Check package version 33 | env: 34 | IS_PRERELEASE: ${{ github.event.release.prerelease }} 35 | run: | 36 | PACKAGE_VERSION=$(node -p "require('./package.json').version") 37 | echo "Package version: $PACKAGE_VERSION" 38 | 39 | if [[ "$IS_PRERELEASE" == "true" ]]; then 40 | # For prereleases, version must contain -beta 41 | if [[ "$PACKAGE_VERSION" != *"-beta"* ]]; then 42 | echo "❌ Prerelease failed: Package version '$PACKAGE_VERSION' must contain '-beta'" 43 | exit 1 44 | fi 45 | echo "✅ Prerelease version check passed" 46 | else 47 | # For releases, version must NOT contain -beta 48 | if [[ "$PACKAGE_VERSION" == *"-beta"* ]]; then 49 | echo "❌ Release failed: Package version '$PACKAGE_VERSION' cannot contain '-beta'" 50 | exit 1 51 | fi 52 | echo "✅ Release version check passed" 53 | fi 54 | - name: Publish to npm 55 | env: 56 | IS_PRERELEASE: ${{ github.event.release.prerelease }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | run: | 59 | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 60 | if [[ "$IS_PRERELEASE" == "true" ]]; then 61 | npm publish --tag beta 62 | else 63 | npm publish 64 | fi 65 | - name: Send Discord Notification 66 | if: ${{ !github.event.release.prerelease }} 67 | uses: sarisia/actions-status-discord@v1 68 | with: 69 | webhook: ${{ secrets.DISCORD_WEBHOOK }} 70 | nodetail: true 71 | url: ${{ github.event.release.html_url }} 72 | title: 🚀 **RELEASE** **@digital-alchemy/hass ${{ github.event.release.tag_name }}** published 73 | content: | 74 | (${{ github.event.release.tag_name }}) **${{ github.event.release.name }}** 75 | ${{ github.event.release.body }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /build 4 | /tmp 5 | /backup 6 | /out-tsc 7 | .VSCodeCounter 8 | apps/**/yarn.lock 9 | # Private keys 10 | **/*vsix 11 | 12 | # dependencies 13 | **/node_modules 14 | config.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # misc 33 | .nx 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | yarn-error.log 40 | testem.log 41 | /typings 42 | /*.tar 43 | 44 | # System Files 45 | .DS_Store 46 | .npmrc 47 | Thumbs.db 48 | globalConfig.json 49 | report.*.json 50 | 51 | # tools/security 52 | tools/security/*pem 53 | .gitsecret/keys/random_seed 54 | 55 | # React Native 56 | 57 | ## Xcode 58 | 59 | **/ios/**/build/ 60 | **/ios/**/*.pbxuser 61 | !default.pbxuser 62 | *.mode1v3 63 | !default.mode1v3 64 | *.mode2v3 65 | !default.mode2v3 66 | *.perspectivev3 67 | !default.perspectivev3 68 | xcuserdata 69 | *.xccheckout 70 | *.moved-aside 71 | DerivedData 72 | *.hmap 73 | *.ipa 74 | *.xcuserstate 75 | 76 | ## Android 77 | 78 | **/android/**/build/ 79 | **/android/**/.gradle 80 | **/android/**/local.properties 81 | **/android/**/*.iml 82 | 83 | ## BUCK 84 | 85 | buck-out/ 86 | \.buckd/ 87 | *.keystore 88 | !debug.keystore 89 | 90 | ## fastlane 91 | # 92 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 93 | # screenshots whenever they are needed. 94 | # For more information about the recommended setup visit: 95 | # https://docs.fastlane.tools/best-practices/source-control/ 96 | # 97 | */fastlane/report.xml 98 | */fastlane/Preview.html 99 | */fastlane/screenshots 100 | 101 | ## Bundle artifact 102 | *.jsbundle 103 | 104 | ## CocoaPods 105 | **/ios/Pods/ 106 | / 107 | 108 | apps/**/artifacts 109 | .config 110 | /.*rc 111 | !/.auto-config-testrc 112 | !/.prettierrc 113 | !/.entity-creationrc 114 | e2e/hass/config 115 | !/.scene-managerrc 116 | !/.pino-prettyrc 117 | /.storage 118 | docker/homeassistant/config 119 | docker/homeassistant/config 120 | bin/ 121 | 122 | 123 | .nx/cache 124 | review.zip 125 | dynamic.d.ts 126 | hass/config 127 | test.d.ts 128 | .yarn/install-state.gz 129 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude testing files from npm package 2 | src/testing/ 3 | src/testing/**/* 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all", 4 | "jsxSingleQuote": false, 5 | "printWidth": 100, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "zeroconf" 4 | ], 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | nodeLinker: node-modules 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Zoe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/Digital-Alchemy-TS/hass/graph/badge.svg?token=LYUQ1FQ71D)](https://codecov.io/gh/Digital-Alchemy-TS/hass) 2 | [![version](https://img.shields.io/github/package-json/version/Digital-Alchemy-TS/hass)](https://www.npmjs.com/package/@digital-alchemy/hass) 3 | [![stars](https://img.shields.io/github/stars/Digital-Alchemy-TS/hass)](https://github.com/Digital-Alchemy-TS/hass) 4 | 5 | --- 6 | 7 |
8 | 9 | ![Digital Alchemy](https://raw.githubusercontent.com/Digital-Alchemy-TS/.github/main/profile/github-logo.png) 10 | ![Digital Alchemy](https://avatars.githubusercontent.com/u/13844975?s=125&v=4) 11 | 12 |
13 | 14 | ## Install 15 | 16 | Add as a dependency, and add to your imports. Nice and easy 17 | 18 | ```bash 19 | yarn add @digital-alchemy/hass 20 | yarn add -D @digital-alchemy/type-writer 21 | ``` 22 | 23 | ## Introduction 24 | 25 | `@digital-alchemy/hass` builds off the Digital Alchemy core framework to introduce API bindings for Home Assistant, exposing functionality for event subscriptions, websocket & rest api interactions, and more! 26 | 27 | The library is able to be customized with the [type-writer](https://github.com/Digital-Alchemy-TS/type-writer) script in order to customize the editor experience for your individual Home Assistant instance. 28 | All services with their parameter inputs are available to call, as well as tools to create long term type safe entity references. 29 | 30 | - [📚 Extended docs](https://docs.digital-alchemy.app) 31 | 32 | ## Configuration 33 | 34 | ### Connection Details 35 | 36 | If you are running the code within a Home Assistant addon (ex: Code Server or Code Runner), the library will automatically configure from environment variables. 37 | 38 | All other environments should define credentials in a `.env` file or provide them via environment variables - 39 | 40 | ``` 41 | HASS_BASE_URL=http://localhost:8123 42 | HASS_TOKEN= 43 | ``` 44 | 45 | ### Customizing Types 46 | 47 | The `type-writer` script utilizes `hass` under the hood, and will load configuration from the same sources. 48 | In order to run the script, execute this command from repository root 49 | 50 | ```bash 51 | npx type-writer 52 | ``` 53 | 54 | This will create / update the `src/hass` folder inside your project with the latest type definitions for your project. 55 | These have **NO EFFECT** on the way your app performs at runtime, and can be updated as frequently as you like. 56 | 57 | ## Usage 58 | 59 | ### Load 60 | 61 | Once credentials are set and you have your type definitions generated, you can add the library to your existing project 62 | 63 | ```typescript 64 | import { LIB_HASS } from "@digital-alchemy/hass"; 65 | 66 | // application 67 | const MY_APP = CreateApplication({ 68 | libraries: [LIB_HASS], 69 | name: "home_automation", 70 | }); 71 | 72 | // library 73 | export const MY_LIBRARY = CreateLibrary({ 74 | depends: [LIB_HASS], 75 | name: "special_logic", 76 | }); 77 | ``` 78 | 79 | ### Build logic 80 | 81 | The `hass` property will be available via `TServiceParams` in your code 82 | 83 | ```typescript 84 | import { TServiceParams } from "@digital-alchemy/core"; 85 | 86 | // now available here vvvv 87 | export function ExampleService({ hass, logger }: TServiceParams) { 88 | const mySwitch = hass.refBy.id("switch.my_switch"); 89 | 90 | // utilize event patterns to trigger logic 91 | hass.refBy.id("binary_sensor.recent_activity_detected").onUpdate((new_state, old_state) => { 92 | 93 | // quickly make logic tests 94 | if (new_state.state === "on" && mySwitch.state === "off") { 95 | 96 | // execute service calls via entities 97 | mySwitch.turn_on(); 98 | 99 | } else { 100 | // get type safe access to the full list of services available on your instance 101 | hass.call.switch.turn_off({ area: "living_room" }); 102 | 103 | } 104 | }); 105 | } 106 | ``` 107 | 108 | ## Questions / Issues? 109 | 110 | [![discord](https://img.shields.io/discord/1219758743848489147?label=Discord&logo=discord)](https://discord.gg/JkZ35Gv97Y) 111 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | version: "0.2" 2 | ignorePaths: [] 3 | dictionaryDefinitions: [] 4 | dictionaries: [] 5 | words: 6 | - cobertura 7 | - codeowners 8 | - endregion 9 | - entites 10 | - hass 11 | - hassio 12 | - homeassistant 13 | - homekit 14 | - hvac 15 | - macaddress 16 | - gbit 17 | - cbar 18 | - mbar 19 | - quickboot 20 | - websockets 21 | - rgbw 22 | - rgbww 23 | - hacs 24 | - rrule 25 | - rtsp 26 | - sonarjs 27 | - mbit 28 | - datetime 29 | - kbit 30 | - postdata 31 | - ssdp 32 | - systype 33 | - tvshow 34 | - zeroconf 35 | - templow 36 | - partlycloudy 37 | ignoreWords: [] 38 | import: [] 39 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import importPlugin from "eslint-plugin-import"; 2 | import jsonc from "eslint-plugin-jsonc"; 3 | import noUnsanitized from "eslint-plugin-no-unsanitized"; 4 | import simpleImportSort from "eslint-plugin-simple-import-sort"; 5 | import sortKeysFix from "eslint-plugin-sort-keys-fix"; 6 | import unicorn from "eslint-plugin-unicorn"; 7 | import prettier from "eslint-plugin-prettier"; 8 | import { fixupPluginRules } from "@eslint/compat"; 9 | import globals from "globals"; 10 | import tsParser from "@typescript-eslint/parser"; 11 | import sonarjs from "eslint-plugin-sonarjs"; 12 | import path from "node:path"; 13 | import { fileURLToPath } from "node:url"; 14 | import js from "@eslint/js"; 15 | import { FlatCompat } from "@eslint/eslintrc"; 16 | 17 | const __filename = fileURLToPath(import.meta.url); 18 | const __dirname = path.dirname(__filename); 19 | const compat = new FlatCompat({ 20 | baseDirectory: __dirname, 21 | recommendedConfig: js.configs.recommended, 22 | allConfig: js.configs.all, 23 | }); 24 | 25 | export default [ 26 | sonarjs.configs.recommended, 27 | { 28 | plugins: { 29 | import: fixupPluginRules(importPlugin), 30 | jsonc, 31 | "no-unsanitized": noUnsanitized, 32 | "simple-import-sort": simpleImportSort, 33 | "sort-keys-fix": sortKeysFix, 34 | unicorn, 35 | prettier, 36 | }, 37 | languageOptions: { 38 | globals: { ...globals.node }, 39 | }, 40 | }, 41 | ...compat 42 | .extends( 43 | "plugin:@typescript-eslint/recommended", 44 | "plugin:jsonc/recommended-with-jsonc", 45 | "plugin:prettier/recommended", 46 | "plugin:@cspell/recommended", 47 | ) 48 | .map((config) => ({ ...config, files: ["src/**/*.mts"] })), 49 | { 50 | files: ["src/**/*.mts"], 51 | languageOptions: { 52 | parser: tsParser, 53 | ecmaVersion: 5, 54 | sourceType: "script", 55 | parserOptions: { 56 | project: ["tsconfig.json"], 57 | }, 58 | }, 59 | rules: { 60 | "prettier/prettier": "error", 61 | "unicorn/switch-case-braces": "off", 62 | "unicorn/prefer-module": "off", 63 | "@typescript-eslint/no-magic-numbers": "warn", 64 | "unicorn/no-object-as-default-parameter": "off", 65 | "unicorn/no-null": "off", 66 | "unicorn/no-empty-file": "off", 67 | "sonarjs/prefer-single-boolean-return": "off", 68 | "unicorn/no-array-callback-reference": "off", 69 | "sonarjs/prefer-nullish-coalescing": "off", 70 | "unicorn/no-process-exit": "off", 71 | "sonarjs/no-skipped-test": "off", 72 | "unicorn/no-await-expression-member": "off", 73 | "@typescript-eslint/consistent-type-imports": "error", 74 | "sonarjs/no-invalid-await": "off", 75 | "sonarjs/no-nested-functions": "off", 76 | "sonarjs/no-empty-function": "off", 77 | "@typescript-eslint/no-unused-expressions": "off", 78 | "unicorn/expiring-todo-comments": "off", 79 | "sonarjs/no-unused-expressions": "off", 80 | "unicorn/no-useless-undefined": "off", 81 | "@typescript-eslint/unbound-method": "error", 82 | "sonarjs/sonar-no-fallthrough": "off", 83 | "import/no-extraneous-dependencies": [ 84 | "error", 85 | { 86 | "packageDir": "./" 87 | } 88 | ], 89 | "sonarjs/prefer-immediate-return": "off", 90 | "unicorn/prevent-abbreviations": "off", 91 | "no-case-declarations": "off", 92 | "no-async-promise-executor": "off", 93 | "unicorn/prefer-node-protocol": "off", 94 | "unicorn/no-array-for-each": "off", 95 | "sonarjs/no-clear-text-protocols": "off", 96 | "unicorn/import-style": "off", 97 | "sonarjs/fixme-tag": "off", 98 | "sort-keys-fix/sort-keys-fix": "warn", 99 | "unicorn/prefer-event-target": "off", 100 | "simple-import-sort/imports": "warn", 101 | "sonarjs/no-misused-promises": "off", 102 | "sonarjs/no-commented-code": "off", 103 | "sonarjs/todo-tag": "off", 104 | "simple-import-sort/exports": "warn", 105 | "no-console": [ 106 | "error" 107 | ], 108 | "@typescript-eslint/no-unnecessary-type-constraint": "off", 109 | "@typescript-eslint/no-unused-vars": [ 110 | "warn", 111 | { 112 | "varsIgnorePattern": "_|logger" 113 | } 114 | ], 115 | "@typescript-eslint/no-explicit-any": "error" 116 | }, 117 | }, 118 | // module definitions 119 | { 120 | files: ["src/**/*.module.mts"], 121 | languageOptions: { 122 | parser: tsParser, 123 | ecmaVersion: 5, 124 | sourceType: "script", 125 | parserOptions: { 126 | project: ["tsconfig.json"], 127 | }, 128 | }, 129 | rules: { 130 | "@typescript-eslint/no-magic-numbers": "off", 131 | }, 132 | }, 133 | { 134 | files: ["src/**/*.spec.mts"], 135 | languageOptions: { 136 | parser: tsParser, 137 | ecmaVersion: 5, 138 | sourceType: "script", 139 | parserOptions: { 140 | project: ["tsconfig.json"], 141 | }, 142 | }, 143 | rules: { 144 | "@typescript-eslint/unbound-method": "off", 145 | "@typescript-eslint/no-magic-numbers": "off", 146 | "sonarjs/no-duplicate-string": "off", 147 | "sonarjs/no-unused-collection": "warn", 148 | "unicorn/consistent-function-scoping": "off", 149 | "sonarjs/prefer-promise-shorthand": "off" 150 | }, 151 | }, 152 | ]; 153 | -------------------------------------------------------------------------------- /hass/README.md: -------------------------------------------------------------------------------- 1 | ## Container Details 2 | 3 | - **username**: digital-alchemy 4 | - **password**: password 5 | -------------------------------------------------------------------------------- /hass/compress.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd hass || exit 3 | rm ./reference.tar.gz 4 | tar -czvf ./reference.tar.gz ./config 5 | -------------------------------------------------------------------------------- /hass/decompress.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd hass/ || exit 3 | tar -xzvf ./reference.tar.gz 4 | -------------------------------------------------------------------------------- /hass/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | homeassistant: 4 | container_name: digital_alchemy_e2e_homeassistant 5 | image: homeassistant/home-assistant:stable 6 | network_mode: "host" 7 | volumes: 8 | - ./config:/config 9 | restart: unless-stopped 10 | -------------------------------------------------------------------------------- /hass/reference.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Digital-Alchemy-TS/hass/6bd527a70bc9a27cb5b5a5358c905228d11d2931/hass/reference.tar.gz -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@digital-alchemy/hass", 4 | "repository": "https://github.com/Digital-Alchemy-TS/hass", 5 | "homepage": "https://docs.digital-alchemy.app", 6 | "version": "25.10.20", 7 | "description": "Typescript APIs for Home Assistant. Includes rest & websocket bindings", 8 | "scripts": { 9 | "build": "rm -rf dist/; tsc", 10 | "lint": "eslint src", 11 | "test": "vitest", 12 | "test:coverage": "vitest run --coverage", 13 | "prepublishOnly": "tsc --project ./tsconfig.lib.json", 14 | "upgrade": "yarn up '@digital-alchemy/*'" 15 | }, 16 | "bugs": { 17 | "email": "bugs@digital-alchemy.app", 18 | "url": "https://github.com/Digital-Alchemy-TS/hass/issues/new/choose" 19 | }, 20 | "keywords": [ 21 | "nodejs", 22 | "home-automation", 23 | "automation", 24 | "typescript", 25 | "websocket", 26 | "home-assistant", 27 | "digital-alchemy" 28 | ], 29 | "bin": { 30 | "mock-assistant": "./scripts/mock-assistant.sh" 31 | }, 32 | "funding": [ 33 | { 34 | "url": "https://github.com/sponsors/zoe-codez", 35 | "type": "GitHub" 36 | }, 37 | { 38 | "url": "https://ko-fi.com/zoe_codez", 39 | "type": "ko-fi" 40 | } 41 | ], 42 | "author": { 43 | "url": "https://github.com/zoe-codez", 44 | "name": "Zoe Codez" 45 | }, 46 | "files": [ 47 | "dist/**/*", 48 | "scripts/**/*", 49 | "src/**/*" 50 | ], 51 | "engines": { 52 | "node": ">=20" 53 | }, 54 | "exports": { 55 | ".": "./dist/index.mjs", 56 | "./mock-assistant": "./dist/mock_assistant/index.mjs", 57 | "./dev-types": "./dist/dev/index.mjs" 58 | }, 59 | "license": "MIT", 60 | "devDependencies": { 61 | "@cspell/eslint-plugin": "^9.2.1", 62 | "@digital-alchemy/core": "^25.8.21", 63 | "@digital-alchemy/synapse": "^25.8.21", 64 | "@digital-alchemy/type-writer": "^25.10.12", 65 | "@eslint/compat": "^1.4.0", 66 | "@eslint/eslintrc": "^3.3.1", 67 | "@eslint/js": "^9.38.0", 68 | "@faker-js/faker": "^10.1.0", 69 | "@types/js-yaml": "^4.0.9", 70 | "@types/node": "^24.8.1", 71 | "@types/node-cron": "^3.0.11", 72 | "@types/semver": "^7.7.1", 73 | "@types/ws": "^8.18.1", 74 | "@typescript-eslint/eslint-plugin": "8.46.1", 75 | "@typescript-eslint/parser": "8.46.1", 76 | "@vitest/coverage-v8": "^3.2.4", 77 | "dayjs": "^1.11.18", 78 | "dotenv": "^17.2.3", 79 | "eslint": "9.38.0", 80 | "eslint-config-prettier": "10.1.8", 81 | "eslint-plugin-import": "^2.32.0", 82 | "eslint-plugin-jsonc": "^2.21.0", 83 | "eslint-plugin-no-unsanitized": "^4.1.4", 84 | "eslint-plugin-prettier": "^5.5.4", 85 | "eslint-plugin-security": "^3.0.1", 86 | "eslint-plugin-simple-import-sort": "^12.1.1", 87 | "eslint-plugin-sonarjs": "^3.0.5", 88 | "eslint-plugin-sort-keys-fix": "^1.1.2", 89 | "eslint-plugin-unicorn": "^61.0.2", 90 | "node-cron": "^4.2.1", 91 | "prettier": "^3.6.2", 92 | "semver": "^7.7.3", 93 | "tsx": "^4.20.6", 94 | "typescript": "^5.9.3", 95 | "uuid": "^13.0.0", 96 | "vitest": "^3.2.4", 97 | "ws": "^8.18.3" 98 | }, 99 | "peerDependencies": { 100 | "@digital-alchemy/core": "*" 101 | }, 102 | "dependencies": { 103 | "dayjs": "^1.11.18", 104 | "semver": "^7.7.3", 105 | "type-fest": "^5.1.0", 106 | "uuid": "^13.0.0", 107 | "ws": "^8.18.3" 108 | }, 109 | "packageManager": "yarn@4.10.3" 110 | } 111 | -------------------------------------------------------------------------------- /scripts/mock-assistant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FULL_PATH=$(realpath "$0") 4 | FILE_BASE=$(dirname "$FULL_PATH") 5 | npx tsx "$FILE_BASE/../src/mock_assistant/main.ts" "$1" 6 | -------------------------------------------------------------------------------- /src/dev/index.mts: -------------------------------------------------------------------------------- 1 | export * from "./mappings.mts"; 2 | export * from "./registry.mts"; 3 | export * from "./services.mts"; 4 | -------------------------------------------------------------------------------- /src/dev/mappings.mts: -------------------------------------------------------------------------------- 1 | declare module "../user.mts" { 2 | export interface HassUniqueIdMapping { 3 | "5622d76001a335e3ea893c4d60d31b3d-next_dawn": "sensor.sun_next_dawn"; 4 | "5622d76001a335e3ea893c4d60d31b3d-next_dusk": "sensor.sun_next_dusk"; 5 | "5622d76001a335e3ea893c4d60d31b3d-next_midnight": "sensor.sun_next_midnight"; 6 | "5622d76001a335e3ea893c4d60d31b3d-next_noon": "sensor.sun_next_noon"; 7 | "5622d76001a335e3ea893c4d60d31b3d-next_rising": "sensor.sun_next_rising"; 8 | "5622d76001a335e3ea893c4d60d31b3d-next_setting": "sensor.sun_next_setting"; 9 | "5622d76001a335e3ea893c4d60d31b3d-solar_elevation": "sensor.sun_solar_elevation"; 10 | "6d8acf36200c5ff8d2d9bb1b1f1dbe00c7eb5b7540103fd90c9a035f82967431": "button.start_white_noise"; 11 | "5622d76001a335e3ea893c4d60d31b3d-solar_azimuth": "sensor.sun_solar_azimuth"; 12 | "5622d76001a335e3ea893c4d60d31b3d-solar_rising": "sensor.sun_solar_rising"; 13 | digital_alchemy: "person.digital_alchemy"; 14 | "6acd101923c0460fc31bad82c4efa140": "todo.shopping_list"; 15 | "4a7fc2592d3a98e0eed8cbc73e839c1c": "tts.google_en_com"; 16 | hass_e2e_is_online: "binary_sensor.hass_e2e_online"; 17 | e1806fdc93296bbd5ab42967003cd38729ff9ba6cfeefc3e15a03ad01ac894fe: "sensor.magic"; 18 | a6e8373221727e197144ba689d7606d4be6f609f2fd0fd8e17516548780465ab: "binary_sensor.toggles"; 19 | "413eb6d69bbec134a07a6d32effd3c3763955e611f43256600cca40725276816": "switch.bedroom_lamp"; 20 | "06d5a22e681ee9c668f8563bd3108853fb053c43342131782afe989090c4ced9": "switch.kitchen_cabinets"; 21 | "27b4fc99f35bbdd1a07173caff5b52f86e3bc342db96f48427e47980b0fb6b49": "switch.living_room_mood_lights"; 22 | "8eb8c1f8c760e97cfa49a0a29cd6891313a1e9a45dd046a556a9f317778cf50a": "switch.porch_light"; 23 | "05ecbbc6111791b6baacbbb60397db14": "calendar.united_states_tx"; 24 | date_example_unique_id: "date.example"; 25 | datetime_example_unique_id: "datetime.example"; 26 | lock_example_unique_id: "lock.example"; 27 | number_example_unique_id: "number.example"; 28 | select_example_unique_id: "select.example"; 29 | text_example_unique_id: "text.example"; 30 | time_example_unique_id: "time.example"; 31 | } 32 | 33 | export interface HassZoneMapping { 34 | test: true; 35 | } 36 | 37 | export interface HassDomainMapping { 38 | automation: "automation.example"; 39 | binary_sensor: "binary_sensor.hass_e2e_online" | "binary_sensor.toggles"; 40 | button: "button.example"; 41 | calendar: "calendar.united_states_tx"; 42 | date: "date.example"; 43 | datetime: "datetime.example"; 44 | light: "light.bedroom_ceiling_fan"; 45 | lock: "lock.example"; 46 | number: "number.example"; 47 | person: "person.digital_alchemy"; 48 | scene: "scene.games_room_auto"; 49 | select: "select.example"; 50 | sensor: 51 | | "sensor.magic" 52 | | "sensor.sun_next_dawn" 53 | | "sensor.sun_next_dusk" 54 | | "sensor.sun_next_midnight" 55 | | "sensor.sun_next_noon" 56 | | "sensor.sun_next_rising" 57 | | "sensor.sun_next_setting"; 58 | sun: "sun.sun"; 59 | switch: 60 | | "switch.bedroom_lamp" 61 | | "switch.kitchen_cabinets" 62 | | "switch.living_room_mood_lights" 63 | | "switch.porch_light"; 64 | text: "text.example"; 65 | time: "time.example"; 66 | todo: "todo.shopping_list"; 67 | tts: "tts.google_en_com"; 68 | zone: "zone.home"; 69 | } 70 | 71 | export interface HassPlatformMapping { 72 | _sun: 73 | | "sensor.sun_next_dawn" 74 | | "sensor.sun_next_dusk" 75 | | "sensor.sun_next_midnight" 76 | | "sensor.sun_next_noon" 77 | | "sensor.sun_next_rising" 78 | | "sensor.sun_next_setting" 79 | | "sensor.sun_solar_elevation" 80 | | "sensor.sun_solar_azimuth" 81 | | "sensor.sun_solar_rising"; 82 | _person: "person.digital_alchemy"; 83 | _shopping_list: "todo.shopping_list"; 84 | _google_translate: "tts.google_en_com"; 85 | _synapse: 86 | | "binary_sensor.hass_e2e_online" 87 | | "sensor.magic" 88 | | "button.example" 89 | | "binary_sensor.toggles" 90 | | "switch.bedroom_lamp" 91 | | "switch.kitchen_cabinets" 92 | | "switch.living_room_mood_lights" 93 | | "switch.porch_light"; 94 | _holiday: "calendar.united_states_tx"; 95 | } 96 | 97 | export interface HassDeviceMapping { 98 | _308e39cf50a9fc6c30b4110724ed1f2e: 99 | | "sensor.sun_next_dawn" 100 | | "sensor.sun_next_dusk" 101 | | "sensor.sun_next_midnight" 102 | | "sensor.sun_next_noon" 103 | | "sensor.sun_next_rising" 104 | | "sensor.sun_next_setting" 105 | | "button.example" 106 | | "sensor.sun_solar_elevation" 107 | | "sensor.sun_solar_azimuth" 108 | | "sensor.sun_solar_rising"; 109 | _e58841e47cf86097b310316e55d6bb12: "calendar.united_states_tx"; 110 | } 111 | 112 | export interface HassAreaMapping { 113 | _test: "switch.living_room_mood_lights"; 114 | _living_room: "switch.living_room_mood_lights"; 115 | _kitchen: "switch.kitchen_cabinets"; 116 | _bedroom: "switch.bedroom_lamp" | "light.bedroom_ceiling_fan"; 117 | } 118 | 119 | export interface HassLabelMapping { 120 | _synapse: 121 | | "binary_sensor.hass_e2e_online" 122 | | "sensor.magic" 123 | | "binary_sensor.toggles" 124 | | "switch.bedroom_lamp" 125 | | "switch.kitchen_cabinets" 126 | | "switch.living_room_mood_lights" 127 | | "switch.porch_light"; 128 | _test: never; 129 | } 130 | 131 | export interface HassFloorMapping { 132 | _downstairs: "switch.kitchen_cabinets" | "switch.living_room_mood_lights"; 133 | _upstairs: "switch.bedroom_lamp"; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/hass.module.mts: -------------------------------------------------------------------------------- 1 | import { CreateLibrary } from "@digital-alchemy/core"; 2 | 3 | import { 4 | Area, 5 | Backup, 6 | CallProxy, 7 | Configure, 8 | Device, 9 | EntityManager, 10 | EventsService, 11 | FetchAPI, 12 | FetchInternals, 13 | Floor, 14 | HassDiagnosticsService, 15 | IDByExtension, 16 | Label, 17 | ReferenceService, 18 | Registry, 19 | WebsocketAPI, 20 | Zone, 21 | } from "./services/index.mts"; 22 | 23 | export const LIB_HASS = CreateLibrary({ 24 | configuration: { 25 | /** 26 | * Where to reach Home Assistant at 27 | * 28 | * Will auto detect inside an addon 29 | */ 30 | BASE_URL: { 31 | default: "http://homeassistant.local:8123", 32 | description: "Url to reach Home Assistant at", 33 | type: "string", 34 | }, 35 | 36 | /** 37 | * Setting this to true will tell hass.diagnostics to create the related channels & start emitting 38 | */ 39 | EMIT_DIAGNOSTICS: { 40 | default: false, 41 | description: [ 42 | "Enable the creation of diagnostics channels", 43 | "Value read at bootstrap, cannot be set by env or at runtime", 44 | ], 45 | type: "boolean", 46 | }, 47 | 48 | /** 49 | * When adding new integrations, app will receive 1 update event for everything that changes. 50 | * This can result in a flood of updates where only a single one is needed at the very end. 51 | * 52 | * This setting helps control that. 53 | */ 54 | EVENT_DEBOUNCE_MS: { 55 | default: 50, 56 | description: "Debounce reactions to registry changes", 57 | type: "number", 58 | }, 59 | 60 | /** 61 | * ## ACKNOWLEDGE ME 62 | * 63 | * Home Assistant **should** respond to all sent messages with a reply to confirm it was received. 64 | * 65 | * If this does not happen, then a warning will be emitted into the logs 66 | */ 67 | EXPECT_RESPONSE_AFTER: { 68 | default: 5, 69 | description: 70 | "If sendMessage was set to expect a response, a warning will be emitted after this delay if one is not received", 71 | type: "number", 72 | }, 73 | 74 | /** 75 | * This is reflected in type-writer, make sure to keep your runtime & types in sync 76 | * 77 | * By default disabled entities are removed to help keep file bloat down 78 | */ 79 | FILTER_DISABLED_ENTITIES_ID_BY: { 80 | default: true, 81 | description: "Filter events from disabled entities in id", 82 | type: "boolean", 83 | }, 84 | 85 | /** 86 | * General purpose variable, adds delays to things when retrying 87 | * 88 | * > **NOTE**: this is best set to `0` for unit tests 89 | */ 90 | RETRY_INTERVAL: { 91 | default: 5, 92 | description: "How often to retry connecting on connection failure (seconds)", 93 | type: "number", 94 | }, 95 | 96 | /** 97 | * @internal 98 | */ 99 | SOCKET_AVG_DURATION: { 100 | default: 5, 101 | description: 102 | "How many seconds worth of requests to use in avg for math in REQ_PER_SEC calculations", 103 | type: "number", 104 | }, 105 | 106 | /** 107 | * @internal 108 | */ 109 | SOCKET_CRASH_REQUESTS_PER_SEC: { 110 | default: 500, 111 | description: 112 | "Socket service will commit sudoku if more than this many outgoing messages are sent to Home Assistant in a second. Usually indicates runaway code", 113 | type: "number", 114 | }, 115 | 116 | /** 117 | * @internal 118 | */ 119 | SOCKET_WARN_REQUESTS_PER_SEC: { 120 | default: 300, 121 | description: 122 | "Emit warnings if the home controller attempts to send more than X messages to Home Assistant inside of a second", 123 | type: "number", 124 | }, 125 | 126 | /** 127 | * Long lived access token 128 | */ 129 | TOKEN: { 130 | description: "Long lived access token to Home Assistant", 131 | required: true, 132 | type: "string", 133 | }, 134 | 135 | /** 136 | * Intended to be provided via command line switch. Ex: 137 | * 138 | * ```bash 139 | * $ node dist/main.js --validate-configuration 140 | * ``` 141 | */ 142 | VALIDATE_CONFIGURATION: { 143 | default: false, 144 | description: "Validate the credentials then quit", 145 | type: "boolean", 146 | }, 147 | }, 148 | name: "hass", 149 | // no internal dependency ones first 150 | priorityInit: ["internals", "fetch", "socket"], 151 | services: { 152 | /** 153 | * home assistant areas 154 | */ 155 | area: Area, 156 | 157 | /** 158 | * home assistant backup interactions 159 | */ 160 | backup: Backup, 161 | 162 | /** 163 | * general service calling interface 164 | */ 165 | call: CallProxy, 166 | 167 | /** 168 | * internal tools 169 | */ 170 | configure: Configure, 171 | 172 | /** 173 | * device interactions 174 | */ 175 | device: Device, 176 | 177 | /** 178 | * 179 | */ 180 | diagnostics: HassDiagnosticsService, 181 | 182 | /** 183 | * retrieve and interact with home assistant entities 184 | */ 185 | entity: EntityManager, 186 | 187 | /** 188 | * named event attachments 189 | */ 190 | events: EventsService, 191 | 192 | /** 193 | * rest api commands 194 | */ 195 | fetch: FetchAPI, 196 | 197 | /** 198 | * floors, like groups of areas 199 | */ 200 | floor: Floor, 201 | 202 | /** 203 | * search for entity ids in a type safe way 204 | */ 205 | idBy: IDByExtension, 206 | 207 | /** 208 | * @internal 209 | */ 210 | internals: FetchInternals, 211 | 212 | /** 213 | * home assistant label interactions 214 | */ 215 | label: Label, 216 | 217 | /** 218 | * obtain references to entities 219 | */ 220 | refBy: ReferenceService, 221 | 222 | /** 223 | * interact with the home assistant registry 224 | */ 225 | registry: Registry, 226 | 227 | /** 228 | * websocket interface 229 | */ 230 | socket: WebsocketAPI, 231 | 232 | /** 233 | * zone interactions 234 | */ 235 | zone: Zone, 236 | }, 237 | }); 238 | 239 | declare module "@digital-alchemy/core" { 240 | export interface LoadedModules { 241 | /** 242 | * tools for interacting with home assistant 243 | */ 244 | hass: typeof LIB_HASS; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/helpers/backup.mts: -------------------------------------------------------------------------------- 1 | export interface HomeAssistantBackup { 2 | date: string; 3 | name: string; 4 | path: string; 5 | size: number; 6 | slug: string; 7 | } 8 | export interface BackupResponse { 9 | backing_up: boolean; 10 | backups: HomeAssistantBackup[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/constants.mts: -------------------------------------------------------------------------------- 1 | export const HASS_ENTITY = "HASS_ENTITY"; 2 | export const HASS_ENTITY_GROUP = "HASS_ENTITY_GROUP"; 3 | export const ALL_ENTITIES_UPDATED = "ALL_ENTITIES_UPDATED"; 4 | export const SOCKET_READY = "SOCKET_READY"; 5 | 6 | export enum HassSocketMessageTypes { 7 | auth_required = "auth_required", 8 | auth_ok = "auth_ok", 9 | event = "event", 10 | result = "result", 11 | pong = "pong", 12 | auth_invalid = "auth_invalid", 13 | } 14 | 15 | export const HOME_ASSISTANT_MODULE_CONFIGURATION = "HOME_ASSISTANT_MODULE_CONFIGURATION"; 16 | 17 | export const EARLY_ON_READY = 1; 18 | export const ENTITY_REGISTRY_UPDATED = "ENTITY_REGISTRY_UPDATED"; 19 | export const AREA_REGISTRY_UPDATED = "AREA_REGISTRY_UPDATED"; 20 | export const LABEL_REGISTRY_UPDATED = "LABEL_REGISTRY_UPDATED"; 21 | export const FLOOR_REGISTRY_UPDATED = "FLOOR_REGISTRY_UPDATED"; 22 | export const DEVICE_REGISTRY_UPDATED = "DEVICE_REGISTRY_UPDATED"; 23 | export const ZONE_REGISTRY_UPDATED = "ZONE_REGISTRY_UPDATED"; 24 | -------------------------------------------------------------------------------- /src/helpers/device.mts: -------------------------------------------------------------------------------- 1 | import type { TDeviceId } from "../user.mts"; 2 | 3 | export interface DeviceDetails { 4 | area_id: null | string; 5 | configuration_url: null | string; 6 | config_entries: string[]; 7 | connections: Array; 8 | disabled_by: null; 9 | entry_type: EntryType | null; 10 | hw_version: null | string; 11 | id: TDeviceId; 12 | identifiers: Array>; 13 | labels: string[]; 14 | manufacturer: null | string; 15 | model: null | string; 16 | name_by_user: null | string; 17 | name: string; 18 | serial_number: null; 19 | sw_version: null | string; 20 | via_device_id: TDeviceId; 21 | } 22 | 23 | export enum EntryType { 24 | Service = "service", 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/entity-state.mts: -------------------------------------------------------------------------------- 1 | import type { FIRST, RemoveCallback, TBlackHole } from "@digital-alchemy/core"; 2 | import type { Dayjs } from "dayjs"; 3 | import type { Except } from "type-fest"; 4 | 5 | import type { 6 | ALL_DOMAINS, 7 | ANY_ENTITY, 8 | GetDomain, 9 | iCallService, 10 | PICK_ENTITY, 11 | TAreaId, 12 | TDeviceId, 13 | TLabelId, 14 | TPlatformId, 15 | TRawDomains, 16 | } from "../user.mts"; 17 | import type { SensorUnitOfMeasurement } from "./registry.mts"; 18 | import type { ALL_SERVICE_DOMAINS, ENTITY_STATE } from "./utility.mts"; 19 | 20 | export interface HassEntityContext { 21 | id: string | null; 22 | parent_id: string | null; 23 | user_id: string | null; 24 | } 25 | 26 | type GenericEntityAttributes = { 27 | /** 28 | * Entity groups 29 | */ 30 | entity_id?: ANY_ENTITY[]; 31 | /** 32 | * Human readable name 33 | */ 34 | friendly_name?: string; 35 | }; 36 | 37 | export type EntityHistoryItem = { a: object; s: unknown; lu: number }; 38 | 39 | export type TEntityUpdateCallback = ( 40 | new_state: NonNullable>, 41 | old_state: NonNullable>, 42 | remove: () => TBlackHole, 43 | ) => TBlackHole; 44 | 45 | export type RemovableCallback = ( 46 | callback: TEntityUpdateCallback, 47 | ) => RemoveCallback; 48 | 49 | export type ByIdProxy = ENTITY_STATE & { 50 | entity_id: ENTITY_ID; 51 | /** 52 | * Run callback 53 | */ 54 | onUpdate: RemovableCallback; 55 | /** 56 | * Retrieve state changes for an entity in a date range 57 | */ 58 | history: (from: Dayjs | Date, to: Dayjs | Date) => Promise[]>; 59 | /** 60 | * Run callback once, for next update 61 | */ 62 | once: (callback: TEntityUpdateCallback) => RemoveCallback; 63 | /** 64 | * Will resolve with the next state of the next value. No time limit 65 | */ 66 | nextState: (timeoutMs?: number) => Promise>; 67 | /** 68 | * Will resolve when state 69 | */ 70 | waitForState: (state: string | number, timeoutMs?: number) => Promise>; 71 | /** 72 | * Access the immediate previous entity state 73 | */ 74 | previous: ENTITY_STATE; 75 | /** 76 | * add a listener that can be removed with the removeAllListeners call 77 | * 78 | * for use by other libraries 79 | */ 80 | addListener: (remove: RemoveCallback) => void; 81 | /** 82 | * Remove all resources related to this particular proxy 83 | * 84 | * Will interrupt methods like "nextState", causing them to never return 85 | */ 86 | removeAllListeners: () => void; 87 | } & (GetDomain extends ALL_SERVICE_DOMAINS 88 | ? DomainServiceCalls> 89 | : object); 90 | 91 | type DomainServiceCalls> = { 92 | [SERVICE in Extract]: CallRewrite; 93 | }; 94 | 95 | type CallRewrite< 96 | D extends Extract, 97 | S extends keyof iCallService[D], 98 | > = ( 99 | // @ts-expect-error fix another day, the transformation is valid 100 | data?: Except[typeof FIRST], "entity_id">, 101 | ) => Promise; 102 | 103 | export interface GenericEntityDTO< 104 | ATTRIBUTES extends object = GenericEntityAttributes, 105 | STATE extends unknown = string, 106 | CONTEXT extends HassEntityContext = HassEntityContext, 107 | DOMAIN extends TRawDomains = TRawDomains, 108 | > { 109 | attributes: ATTRIBUTES; 110 | context: CONTEXT; 111 | entity_id: PICK_ENTITY; 112 | last_changed: string; 113 | last_updated: string; 114 | state: STATE; 115 | } 116 | 117 | export interface EventData { 118 | entity_id?: ID; 119 | event?: number; 120 | id?: string; 121 | new_state?: ENTITY_STATE; 122 | old_state?: ENTITY_STATE; 123 | } 124 | export type EntityUpdateEvent< 125 | ID extends ANY_ENTITY = ANY_ENTITY, 126 | CONTEXT extends HassEntityContext = HassEntityContext, 127 | > = { 128 | context: CONTEXT; 129 | data: EventData; 130 | event_type: string; 131 | origin: "local"; 132 | result?: string; 133 | time_fired: Date; 134 | variables: Record; 135 | }; 136 | 137 | export interface EntityDetails { 138 | area_id: TAreaId; 139 | categories: Categories; 140 | config_entry_id: null | string; 141 | device_id: TDeviceId; 142 | disabled_by: string | null; 143 | entity_category: string | null; 144 | entity_id: ENTITY; 145 | has_entity_name: boolean; 146 | hidden_by: string | null; 147 | icon: null; 148 | id: string; 149 | labels: TLabelId[]; 150 | name: null | string; 151 | options: Options; 152 | original_name: null | string; 153 | platform: TPlatformId; 154 | translation_key: null | string; 155 | unique_id: string; 156 | } 157 | 158 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 159 | export interface Categories {} 160 | 161 | export interface Options { 162 | conversation: Conversation; 163 | "sensor.private"?: SensorPrivate; 164 | sensor?: Sensor; 165 | } 166 | 167 | export interface Conversation { 168 | should_expose: boolean; 169 | } 170 | 171 | export interface Sensor { 172 | suggested_display_precision?: number; 173 | display_precision?: null; 174 | unit_of_measurement?: SensorUnitOfMeasurement; 175 | } 176 | 177 | export interface SensorPrivate { 178 | suggested_unit_of_measurement: string; 179 | } 180 | -------------------------------------------------------------------------------- /src/helpers/fetch/calendar.mts: -------------------------------------------------------------------------------- 1 | import type { Dayjs } from "dayjs"; 2 | 3 | import type { PICK_ENTITY } from "../../user.mts"; 4 | 5 | export type CalendarFetchOptions = { 6 | /** 7 | * Calendar(s) to load events for. 8 | */ 9 | calendar: PICK_ENTITY<"calendar"> | PICK_ENTITY<"calendar">[]; 10 | /** 11 | * The end (exclusive) of the event. 12 | */ 13 | end: Date | Dayjs; 14 | /** 15 | * The start (inclusive) of the event. 16 | * 17 | * > Default: now 18 | */ 19 | start?: Date | Dayjs; 20 | }; 21 | 22 | export type RawCalendarEvent = { 23 | /** 24 | * A detailed description of the event. 25 | */ 26 | description?: string; 27 | end: { dateTime: string }; 28 | /** 29 | * A geographic location of the event. 30 | */ 31 | location?: string; 32 | /** 33 | * An optional identifier for a specific instance of a recurring event (required for mutations of recurring events) 34 | */ 35 | recurrence_id?: string; 36 | /** 37 | * A recurrence rule string e.g. `FREQ=DAILY` 38 | */ 39 | rrule?: string; 40 | start: { dateTime: string }; 41 | /** 42 | * A title or summary of the event. 43 | */ 44 | summary: string; 45 | /** 46 | * A unique identifier for the event (required for mutations) 47 | */ 48 | uid?: string; 49 | }; 50 | 51 | export type CalendarEvent = Omit & { 52 | end: Dayjs; 53 | start: Dayjs; 54 | }; 55 | -------------------------------------------------------------------------------- /src/helpers/fetch/configuration.mts: -------------------------------------------------------------------------------- 1 | import type { TAreaId, TFloorId, TLabelId } from "../../user.mts"; 2 | 3 | export interface HassUnitSystem { 4 | length: "mi"; 5 | mass: "lb"; 6 | pressure: "psi"; 7 | temperature: "°F"; 8 | volume: "gal"; 9 | } 10 | 11 | export interface HassConfig { 12 | allowlist_external_dirs: string[]; 13 | allowlist_external_urls: string[]; 14 | components: string[]; 15 | config_dir: string; 16 | config_source: string; 17 | currency: string; 18 | elevation: number; 19 | external_url: string; 20 | internal_url: string; 21 | latitude: number; 22 | location_name: string; 23 | longitude: number; 24 | safe_mode: string; 25 | state: string; 26 | time_zone: string; 27 | unit_system: HassUnitSystem; 28 | version: string; 29 | whitelist_external_dirs: string[]; 30 | } 31 | 32 | export type CheckConfigResult = 33 | | { 34 | errors: null; 35 | result: "valid"; 36 | } 37 | | { 38 | errors: string; 39 | result: "invalid"; 40 | }; 41 | 42 | export type AreaDetails = AreaCreate & { 43 | area_id: TAreaId; 44 | }; 45 | 46 | export type AreaCreate = { 47 | floor_id?: TFloorId; 48 | aliases?: string[]; 49 | icon?: string; 50 | labels?: TLabelId[]; 51 | name: string; 52 | picture?: string; 53 | }; 54 | 55 | export interface ConfigEntry { 56 | entry_id: string; 57 | domain: string; 58 | title: string; 59 | source: string; 60 | state: State; 61 | supports_options: boolean; 62 | supports_remove_device: boolean; 63 | supports_unload: boolean; 64 | supports_reconfigure: boolean; 65 | pref_disable_new_entities: boolean; 66 | pref_disable_polling: boolean; 67 | disabled_by: null; 68 | reason: null | string; 69 | } 70 | 71 | export enum State { 72 | Loaded = "loaded", 73 | NotLoaded = "not_loaded", 74 | SetupRetry = "setup_retry", 75 | } 76 | -------------------------------------------------------------------------------- /src/helpers/fetch/index.mts: -------------------------------------------------------------------------------- 1 | export * from "./calendar.mts"; 2 | export * from "./configuration.mts"; 3 | export * from "./server-log.mts"; 4 | export * from "./service-list.mts"; 5 | export * from "./weather-forecasts.mts"; 6 | -------------------------------------------------------------------------------- /src/helpers/fetch/server-log.mts: -------------------------------------------------------------------------------- 1 | // { 2 | // "0": { 3 | // "name": "homeassistant.util.yaml.loader", 4 | // "message": [ 5 | // "mapping values are not allowed here\n in \"/config/configuration.yaml\", line 71, column 8" 6 | // ], 7 | // "level": "ERROR", 8 | // "source": [ 9 | // "util/yaml/loader.py", 10 | // 127 11 | // ], 12 | // "timestamp": 1638118416.470104, 13 | // "exception": "", 14 | // "count": 2, 15 | // "first_occurred": 1638118343.795454 16 | // } 17 | // } 18 | 19 | export interface HomeAssistantServerLogItem { 20 | count: number; 21 | exception: string; 22 | first_occurred: number; 23 | level: "ERROR" | "WARNING"; 24 | message: string[]; 25 | name: string; 26 | source: [string, number]; 27 | timestamp: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/fetch/service-list.mts: -------------------------------------------------------------------------------- 1 | import type { LiteralUnion } from "type-fest"; 2 | 3 | import type { ALL_DOMAINS, TPlatformId } from "../../user.mts"; 4 | import type { ColorMode } from "../features.mts"; 5 | 6 | export interface ServiceListSelectorTarget { 7 | domain?: ALL_DOMAINS; 8 | integration?: TPlatformId; 9 | multiple?: boolean; 10 | } 11 | 12 | export interface ServiceListSelector { 13 | addon?: null; 14 | backup_location?: null; 15 | boolean?: null; 16 | color_rgb?: null; 17 | color_temp?: { unit: "kelvin"; min: number; max: number }; 18 | conversation_agent?: null; 19 | date?: null; 20 | datetime?: null; 21 | entity?: ServiceListSelectorTarget; 22 | icon?: null; 23 | number?: { 24 | max: number; 25 | min: number; 26 | mode?: string; 27 | step?: number; 28 | unit_of_measurement: string; 29 | }; 30 | object?: null; 31 | select?: { 32 | custom_value?: boolean; 33 | multiple?: boolean; 34 | options: Record<"label" | "value", string>[] | string[]; 35 | }; 36 | text?: null | { type: "password" }; 37 | theme?: { include_defaults?: boolean }; 38 | time?: null; 39 | } 40 | 41 | export interface ServiceListFilter { 42 | supported_features?: number[]; 43 | supported_color_modes?: LiteralUnion<`${ColorMode}`, string>[]; 44 | } 45 | 46 | export interface ServiceListFieldDescription { 47 | advanced?: boolean; 48 | default?: unknown; 49 | description?: string; 50 | example?: string | number; 51 | filter?: ServiceListFilter; 52 | name?: string; 53 | required?: boolean; 54 | selector?: ServiceListSelector; 55 | } 56 | 57 | export type ServiceListEntityTarget = { 58 | domain?: ALL_DOMAINS[]; 59 | integration?: TPlatformId; 60 | supported_features?: number[]; 61 | }; 62 | 63 | export interface ServiceListServiceTarget { 64 | device?: { integration?: string }; 65 | entity?: ServiceListEntityTarget[]; 66 | integration?: string; 67 | } 68 | 69 | export interface ServiceListField { 70 | description?: string; 71 | fields: Record; 72 | name?: string; 73 | target?: ServiceListServiceTarget; 74 | response?: ResponseOptional; 75 | } 76 | 77 | export interface ResponseOptional { 78 | optional?: boolean; 79 | } 80 | 81 | export interface HassServiceDTO { 82 | domain: ALL_DOMAINS; 83 | services: Record; 84 | } 85 | -------------------------------------------------------------------------------- /src/helpers/fetch/weather-forecasts.mts: -------------------------------------------------------------------------------- 1 | // Comes from https://www.home-assistant.io/integrations/weather/#action-weatherget_forecasts 2 | 3 | export type WeatherCondition = 4 | | "clear-night" 5 | | "cloudy" 6 | | "fog" 7 | | "hail" 8 | | "lightning" 9 | | "lightning-rainy" 10 | | "partlycloudy" 11 | | "pouring" 12 | | "rainy" 13 | | "snowy" 14 | | "snowy-rainy" 15 | | "sunny" 16 | | "windy" 17 | | "windy-variant" 18 | | "exceptional"; 19 | 20 | export interface WeatherGetForecasts { 21 | /** 22 | * Time of the forecasted conditions. 23 | * Format is YYYY-MM-DDTHH:mm:ss+zz:zz 24 | */ 25 | datetime: string; 26 | /** 27 | * Only set for `twice_daily` forecasts 28 | */ 29 | is_daytime: boolean; 30 | /** 31 | * The apparent (feels-like) temperature in the unit indicated by the `temperature_unite` state attribute. 32 | */ 33 | apparent_temperature: number; 34 | /** 35 | * The cloud coverage in %. 36 | */ 37 | cloud_coverage: number; 38 | /** 39 | * The weather condition 40 | */ 41 | condition: WeatherCondition; 42 | /** 43 | * The dew point temperature in the unit indicated by the `temperature_unite` state attribute. 44 | */ 45 | dew_point: number; 46 | /** 47 | * The relative humidity in %. 48 | */ 49 | humidity: number; 50 | /** 51 | * The probability of precipitation in %; 52 | */ 53 | precipitation_probability: number; 54 | /** 55 | * The precipitation amount in the unit indicated by the `precipitation_unit` state attribute. 56 | */ 57 | precipitation: number; 58 | /** 59 | * The air pressure in the unit indicated by the `pressure_unit` state attribute. 60 | */ 61 | pressure: number; 62 | /** 63 | * The temperature in the unit indicated by the `temperature_unite` state attribute. If `templow` is also provided this is the higher temperature. 64 | */ 65 | temperature: number; 66 | /** 67 | * The lower temperature in the unit indicated by the `temperature_unite` state attribute. 68 | */ 69 | templow: number; 70 | /** 71 | * The UV index. 72 | */ 73 | uv_index: number; 74 | /** 75 | * The wind bearing in azimuth angle (degrees) or 1-3 letter cardinal direction 76 | */ 77 | wind_bearing: number | string; 78 | /** 79 | * The wind gust speed in the unit indicated by the `wind_speed_unit` state attribute. 80 | */ 81 | wind_gust_speed: number; 82 | /** 83 | * The wind speed in the unit indicated by the `wind_speed_unit` state attribute. 84 | */ 85 | wind_speed: number; 86 | } 87 | -------------------------------------------------------------------------------- /src/helpers/id-by.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | ALL_DOMAINS, 3 | ANY_ENTITY, 4 | HassUniqueIdMapping, 5 | PICK_ENTITY, 6 | PICK_FROM_AREA, 7 | PICK_FROM_DEVICE, 8 | PICK_FROM_FLOOR, 9 | PICK_FROM_LABEL, 10 | PICK_FROM_PLATFORM, 11 | TAreaId, 12 | TDeviceId, 13 | TFloorId, 14 | TLabelId, 15 | TPlatformId, 16 | TRawEntityIds, 17 | TUniqueId, 18 | } from "../user.mts"; 19 | 20 | export type IDByInterface = { 21 | area: ( 22 | area: AREA, 23 | ...domains: DOMAIN[] 24 | ) => PICK_FROM_AREA[]; 25 | device: ( 26 | device: DEVICE, 27 | ...domains: DOMAIN[] 28 | ) => PICK_FROM_DEVICE[]; 29 | domain: (domain: DOMAIN) => PICK_ENTITY[]; 30 | floor: ( 31 | floor: FLOOR, 32 | ...domains: DOMAIN[] 33 | ) => PICK_FROM_FLOOR[]; 34 | label: