├── .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 | [](https://codecov.io/gh/Digital-Alchemy-TS/hass)
2 | [](https://www.npmjs.com/package/@digital-alchemy/hass)
3 | [](https://github.com/Digital-Alchemy-TS/hass)
4 |
5 | ---
6 |
7 |
8 |
9 | 
10 | 
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 | [](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: