Compare commits

...
187 Commits
Author SHA1 Message Date
github-actions[bot]andGitHub 77f0eb282b Merge pull request #189 from ioBroker/dependabot/npm_and_yarn/js-yaml-4.2.0
chore(deps-dev): bump js-yaml from 4.1.1 to 4.2.0
2026-06-16 21:01:47 +00:00
dependabot[bot]andGitHub 58c63fcf1a chore(deps-dev): bump js-yaml from 4.1.1 to 4.2.0
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 20:59:34 +00:00
github-actions[bot]andGitHub 4eb196c7c2 Merge pull request #188 from ioBroker/dependabot/npm_and_yarn/alcalzone/release-script-5.2.1
chore(deps-dev): bump @alcalzone/release-script from 5.2.0 to 5.2.1
2026-06-16 20:56:09 +00:00
dependabot[bot]andGitHub c1a354803c chore(deps-dev): bump @alcalzone/release-script from 5.2.0 to 5.2.1
Bumps [@alcalzone/release-script](https://github.com/AlCalzone/release-script) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/AlCalzone/release-script/releases)
- [Changelog](https://github.com/AlCalzone/release-script/blob/master/CHANGELOG.md)
- [Commits](https://github.com/AlCalzone/release-script/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: "@alcalzone/release-script"
  dependency-version: 5.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 20:53:16 +00:00
BluefoxandGitHub a3e29e0580 Merge pull request #182 from ioBroker-Bot/update-from-template-W8917-dependabot-addIgnoreTypesNode-1780171547
[iobroker-bot] Add Dependabot Ignore Rule for @types/node Major Version Updates
2026-06-16 22:52:05 +02:00
github-actions[bot] 85b4f519e1 Update from template: W8917-dependabot-addIgnoreTypesNode 2026-05-30 20:06:17 +00:00
GermanBluefox 21a11d9675 chore: release v2.1.0
* (mokusone) Added optional entity exclude filter with glob patterns, configurable via the admin UI, plus a verbose-logging toggle for inspecting matches
* (@klein0r) Use `/core/` instead of `/api/` when connecting to supervisor directly (e.g., in ha app)
* (@klein0r) Use ENV var SUPERVISOR_TOKEN as fallback for password
2026-05-16 20:08:21 +02:00
GermanBluefox 1883937e4a Bump version 2026-05-16 20:07:53 +02:00
BluefoxandGitHub 962189ba74 Merge pull request #177 from mokusone/upstream-pr-entity-filter
Add optional entity exclude filter with verbose logging
2026-05-16 19:50:10 +02:00
7dd63fa9c8 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-16 19:49:12 +02:00
BluefoxandGitHub 5f57a27ff6 Merge branch 'master' into upstream-pr-entity-filter 2026-05-16 19:20:43 +02:00
GermanBluefox db95c5c6c5 Fixed linter 2026-05-16 19:17:55 +02:00
GermanBluefox 34ac9d9220 Fixed linter 2026-05-16 19:11:49 +02:00
BluefoxandGitHub 262704dc00 Merge pull request #170 from ioBroker-Bot/update-from-template-S6020-addChangelogOld-1777809390
[iobroker-bot] Add CHANGELOG_OLD.md to store older changelog entries
2026-05-16 19:09:09 +02:00
BluefoxandGitHub 12e59a82ef Merge pull request #174 from biglouis/fix/dynamic-attribute-objects-on-state-changed
fix: dynamically create missing attribute objects in state_changed handler
2026-05-16 19:06:47 +02:00
BluefoxandGitHub 488627f37c Merge pull request #166 from klein0r/master
Use /core/ instead of /api/ when connecting to supervisor directly
2026-05-16 19:06:18 +02:00
BluefoxandGitHub 664d55bbe9 Merge pull request #160 from ioBroker-Bot/update-from-template-X0000-updateNodeJsAtTestAndRelease-1775505314
[iobroker-bot] Update Node.js versions in test-and-release workflow
2026-05-16 19:00:43 +02:00
BluefoxandGitHub 3ab1492c4d Merge pull request #156 from ioBroker-Bot/update-from-template-X0000-updateDependabotSettings-1775001747
[iobroker-bot] Update Dependabot Configuration – Add npm Cooldown
2026-05-16 19:00:26 +02:00
GermanBluefox 2e267c03dd Fixing repo checker issues 2026-05-16 18:59:10 +02:00
mokusone 0512ee7cd3 fix(filter): cleanup must work for flat-state entities (no parent channel)
The previous cleanup implementation only iterated objects with
type === 'channel' and read entity_id from native. Both assumptions
break for entities written by older adapter versions: those exist as
flat states under hass.0.entities.<domain>.<entity_id>.<sub> with no
parent channel object and often no native.entity_id.

The admin UI shows them as folders because sub-states are present, but
`iobroker object get hass.0.entities.text.iob_…` returns "not found".
So the cleanup found nothing to delete even with valid patterns.

Refactor: iterate every object under entities.*, derive entity_id from
the first two path components after the namespace prefix, group by
entity_id, then delete each id individually (longest first, no
recursive flag — there is no parent to recurse into). Custom-config
protection still applies per entity group.

Works for both old flat-state and new channel+sub-state structures.
2026-05-15 04:14:48 +02:00
mokusone a40dc58e35 feat(filter): add cleanupExcludedOnStart option to delete stale objects
When enabled, on each adapter start the adapter scans all existing
hass.0.entities.* channels and deletes those whose entity_id matches
any of the configured excludePatterns. This complements the existing
filter (which only prevents new objects from being created) by also
cleaning up objects that were synced before the filter was activated
or before the patterns were extended.

Safety:
- Channels (and any of their sub-states) holding common.custom config
  (history/influxdb/sql) are kept and a warning is logged so the user
  can decide manually.
- Existing deleteStaleObjects() 50% sanity-guard is intentionally NOT
  applied here — the user explicitly opted in to the cleanup.
- Per-id deletion only logs when verboseFilterLog is also active.

Default is false (opt-in). Adds new admin checkbox + i18n keys for all
11 languages (machine translations for non-en/de still pending).
2026-05-15 04:14:41 +02:00
mokusoneandClaude Opus 4.7 a4932a1a8c fix(admin): remove invalid 'multiline' property from excludePatterns
The JSON Config schema for type 'text' enforces additionalProperties:false
and 'multiline' is not a documented property. Its presence caused the
admin UI to fail schema validation and refuse to render the panel.

Multi-line behaviour is already provided by 'minRows: 6' alone — the
schema explicitly states: "Set this attribute to 2 or more if you want
to have a textarea with more than one row."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:12:51 +02:00
mokusone 7b5679a753 docs: document entity exclude filter and add changelog entry
Adds a new README section explaining the glob syntax, providing examples and
covering the verbose-logging toggle. Also adds a WORK IN PROGRESS changelog
entry for the next release.
2026-05-14 11:40:28 +02:00
mokusone fc7efd4768 feat(filter): add verbose filter logging option
New `verboseFilterLog` boolean (default `false`). When enabled, the initial
sync logs every excluded entity_id individually on INFO level. This is
useful when iterating on exclude patterns or verifying that the filter
catches the intended entities.

The verbose output is intentionally limited to the first sync. Subsequent
debouncedSync() calls only emit the aggregate count to avoid log spam
during normal operation. Live `state_changed` events for filtered entities
remain on DEBUG.

Admin UI gets a checkbox below the patterns field; i18n keys added for
all 11 supported languages.
2026-05-14 11:40:28 +02:00
mokusone 288273005a feat(admin): add exclude patterns UI field with i18n
JSON-Config field `excludePatterns` (multiline textarea, 6 rows by default,
full width). Help text explains the syntax: glob with `*`, `#` for comments,
empty = filter inactive.

i18n translations added for all 11 supported languages so the label and
help text are localised consistently with the rest of the admin UI.
2026-05-14 11:40:28 +02:00
mokusone 6889664c28 feat(filter): integrate exclude filter into parseStates and state_changed
Adapter now consults the exclude-pattern list (new `native.excludePatterns`
config option, multi-line string) at two points:

- `parseStates()` skips matching entities entirely. They are not registered
  as ioBroker objects and no state is written. A summary INFO line reports
  the count when the filter dropped anything from a sync.
- The `state_changed` event handler short-circuits matching live updates
  with a DEBUG line, so a filtered entity that still emits state changes
  in HA does not cause any churn in ioBroker.

Patterns are parsed once at adapter start: trimmed, empty lines and lines
starting with `#` are dropped. An empty resulting list means the filter is
inactive — adapter behaviour is then identical to the previous version.

Default config ships with example comments only, so existing installations
see no functional change after the update.
2026-05-14 11:40:28 +02:00
mokusone 9b95ece284 feat(filter): add isExcluded helper with unit tests
Glob-based entity_id matcher used by the upcoming exclude-filter feature.
Single wildcard `*` (matches any sequence including dots), all other characters
matched literally with regex metacharacters escaped. Case-sensitive, anchored.

Empty pattern array always returns false, so the helper is a no-op when the
adapter is not configured to filter anything.

New mocha unit-test suite invoked via `npm run test:unit` (separate from the
existing js-controller-backed test:integration).
2026-05-14 11:40:28 +02:00
github-actions[bot]andGitHub 1f0e93d357 Merge pull request #176 from ioBroker/dependabot/npm_and_yarn/protobufjs-7.5.8
chore(deps-dev): bump protobufjs from 7.5.5 to 7.5.8
2026-05-12 20:39:35 +00:00
dependabot[bot]andGitHub 53e8009ffb chore(deps-dev): bump protobufjs from 7.5.5 to 7.5.8
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.5 to 7.5.8.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.5.8/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.5...protobufjs-v7.5.8)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 20:37:32 +00:00
github-actions[bot]andGitHub 97ca4ca01c Merge pull request #175 from ioBroker/dependabot/npm_and_yarn/protobufjs/utf8-1.1.1
chore(deps-dev): bump @protobufjs/utf8 from 1.1.0 to 1.1.1
2026-05-12 20:21:26 +00:00
dependabot[bot]andGitHub 06bfc37720 chore(deps-dev): bump @protobufjs/utf8 from 1.1.0 to 1.1.1
Bumps [@protobufjs/utf8](https://github.com/dcodeIO/protobuf.js) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/dcodeIO/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dcodeIO/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1)

---
updated-dependencies:
- dependency-name: "@protobufjs/utf8"
  dependency-version: 1.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 20:19:29 +00:00
Torsten Pattberg cb3f490e25 fix: dynamically create missing attribute objects in state_changed handler
When Home Assistant reports a new attribute for an entity after the initial
sync (e.g. source_list or media_channel appearing when a media player becomes
active), setState was called without a corresponding ioBroker object — causing
'has no existing object' warnings.

Fix: check if the attribute object exists in hassObjects before calling
setState. If missing, create it on-the-fly via setForeignObjectAsync before
writing the state.
2026-05-12 15:58:31 +02:00
Matthias KleineandGitHub af1052af03 Merge branch 'master' into master 2026-05-07 10:41:49 +02:00
GermanBluefox 0d39cc9d1d chore: release v2.0.4
* (@GermanBluefox) Tried to keep the custom settings of the objects when updating them with new data from HASS
2026-05-05 14:31:50 +02:00
Matthias KleineandGitHub 47b80f47c2 Merge branch 'master' into master 2026-05-05 12:07:10 +02:00
github-actions[bot]andGitHub c0defc48a5 Merge pull request #171 from ioBroker/dependabot/npm_and_yarn/axios-1.16.0
chore(deps-dev): bump axios from 1.15.0 to 1.16.0
2026-05-05 09:00:06 +00:00
dependabot[bot]andGitHub d525d5b26b chore(deps-dev): bump axios from 1.15.0 to 1.16.0
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 08:05:56 +00:00
GermanBluefox a64ef243f0 Tried to keep the custom settings of the objects when updating them with new data from HASS 2026-05-05 10:01:38 +02:00
github-actions[bot] f4956c5040 Update from template: S6020-addChangelogOld 2026-05-03 11:57:00 +00:00
Matthias Kleine ab85a9311d .dev-server to gitignore 2026-04-28 13:23:10 +02:00
Matthias Kleine 6159ef5c59 Updated home assistant logo 2026-04-28 13:23:02 +02:00
Matthias Kleine 5657211716 Use ENV var SUPERVISOR_TOKEN as fallback for password 2026-04-28 11:15:02 +02:00
Matthias Kleine 7575b129c0 Use /core/ instead of /api/ when connecting to supervisor directly 2026-04-28 11:05:24 +02:00
github-actions[bot]andGitHub 9a24c2be91 Merge pull request #163 from ioBroker/dependabot/npm_and_yarn/protobufjs-7.5.5
chore(deps-dev): bump protobufjs from 7.5.4 to 7.5.5
2026-04-17 06:04:54 +00:00
dependabot[bot]andGitHub d541114da1 chore(deps-dev): bump protobufjs from 7.5.4 to 7.5.5
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.4 to 7.5.5.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.4...protobufjs-v7.5.5)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 05:34:02 +00:00
github-actions[bot]andGitHub eb7b8b0e68 Merge pull request #162 from ioBroker/dependabot/npm_and_yarn/follow-redirects-1.16.0
chore(deps-dev): bump follow-redirects from 1.15.11 to 1.16.0
2026-04-15 20:07:41 +00:00
dependabot[bot]andGitHub e139c9bc27 chore(deps-dev): bump follow-redirects from 1.15.11 to 1.16.0
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 19:06:08 +00:00
github-actions[bot]andGitHub 40550a747b Merge pull request #161 from ioBroker/dependabot/npm_and_yarn/axios-1.15.0
Bump axios from 1.14.0 to 1.15.0
2026-04-12 11:57:13 +00:00
dependabot[bot]andGitHub 9a886b1155 Bump axios from 1.14.0 to 1.15.0
Bumps [axios](https://github.com/axios/axios) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-12 11:55:15 +00:00
github-actions[bot] c19bb8452e Update from template: X0000-updateNodeJsAtTestAndRelease 2026-04-06 19:55:44 +00:00
GermanBluefox 039e856f7a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	io-package.json
2026-04-02 10:32:44 +02:00
GermanBluefox 06cdfbd9e4 chore: release v2.0.3
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
* (@Titanium177) Added roles for states and added debouncing for reading states from hass
2026-04-02 10:32:32 +02:00
GermanBluefox e92d488529 Bump version 2026-04-02 10:32:19 +02:00
github-actions[bot] 8b7f80019e Update from template: X0000-updateDependabotSettings 2026-04-01 00:02:57 +00:00
BluefoxandGitHub d5f874cf84 Merge pull request #137 from ioBroker-Bot/update-from-template-W1081-addLicenseInformation-1762607726
[iobroker-bot] Add license information to io-package.json
2026-03-31 22:23:15 +02:00
BluefoxandGitHub 952680454b Merge branch 'master' into update-from-template-W1081-addLicenseInformation-1762607726 2026-03-31 22:23:05 +02:00
BluefoxandGitHub c4fb2d37aa Merge pull request #136 from ioBroker-Bot/update-from-template-S9006-blockCommitinfo-1760888564
[iobroker-bot] Block .commitinfo by adding to .gitignore
2026-03-31 22:22:29 +02:00
BluefoxandGitHub 1f06d5afac Merge branch 'master' into update-from-template-S9006-blockCommitinfo-1760888564 2026-03-31 22:22:21 +02:00
BluefoxandGitHub a64edae70c Merge pull request #154 from ioBroker/dependabot/github_actions/github/codeql-action-4
Bump github/codeql-action from 2 to 4
2026-03-31 22:21:57 +02:00
BluefoxandGitHub 477e910371 Merge pull request #153 from ioBroker/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-03-31 22:21:49 +02:00
BluefoxandGitHub 959a5f7a3f Merge pull request #146 from ioBroker-Bot/update-from-template-X0000-updateNodeJsAtTestAndRelease-1772981360
[iobroker-bot] Update Node.js versions in test-and-release workflow
2026-03-31 22:21:06 +02:00
dependabot[bot]andGitHub 90f53d8ce8 Bump github/codeql-action from 2 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 20:15:54 +00:00
BluefoxandGitHub 360baf112e Merge pull request #141 from ioBroker-Bot/update-from-template-W1084-removeCommonTitle-1765837585
[iobroker-bot] Remove deprecated common.title from io-package.json
2026-03-31 22:15:52 +02:00
dependabot[bot]andGitHub 164a8c6bee Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 20:15:45 +00:00
BluefoxandGitHub 89a588b73d Merge branch 'master' into update-from-template-W1084-removeCommonTitle-1765837585 2026-03-31 22:15:39 +02:00
BluefoxandGitHub 36f296ee8f Merge pull request #139 from ioBroker-Bot/update-from-template-X0000-randomDependabot-1762823480
[iobroker-bot] Optimize Dependabot Schedule Configuration
2026-03-31 22:15:10 +02:00
BluefoxandGitHub b9e92864a0 Merge pull request #144 from ioBroker-Bot/update-from-template-X0000-setupAutomergeDependabot-1768922970
[iobroker-bot] Setup Auto-Merge for Dependabot PRs
2026-03-31 22:14:34 +02:00
GermanBluefox 906e6d172c chore: release v2.0.2
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
* (@Titanium177) Added roles for states and added debouncing for reading states from hass
2026-03-31 22:11:49 +02:00
GermanBluefox 2f63e3a6dd Bump version 2026-03-31 22:11:34 +02:00
GermanBluefox bc74169c7e Merge https://github.com/ioBroker/ioBroker.hass/pull/129 2026-03-31 22:11:09 +02:00
GermanBluefox fbabfaae94 chore: release v2.0.1
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
2026-03-31 20:15:35 +02:00
GermanBluefox 8b6e297800 Fix CI 2026-03-31 20:13:36 +02:00
GermanBluefox f0b6a23ad6 chore: release v2.0.0
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
2026-03-31 20:10:03 +02:00
GermanBluefox 4905caf6c2 Typos 2026-03-31 20:09:51 +02:00
GermanBluefox 11a4b0bafe Remove settings for claude 2026-03-31 20:07:45 +02:00
GermanBluefox ebca1ad36c MIgrate adapter to Typescript 2026-03-31 19:44:44 +02:00
GermanBluefox 92140fe771 Update packages 2026-03-31 17:00:15 +02:00
github-actions[bot] 1ed00c8778 Update from template: X0000-updateNodeJsAtTestAndRelease 2026-03-08 14:49:50 +00:00
github-actions[bot] d1442504e8 Update from template: X0000-setupAutomergeDependabot 2026-01-20 15:30:00 +00:00
github-actions[bot] 2ca9828262 Update from template: W1084-removeCommonTitle 2025-12-15 22:26:56 +00:00
github-actions[bot] 2e78c1f42b Update from template: X0000-randomDependabot 2025-11-11 01:11:50 +00:00
github-actions[bot] 8dbde9bbcb Update from template: W1081-addLicenseInformation 2025-11-08 13:15:57 +00:00
github-actions[bot] 1b6d9bbeea Update from template: S9006-blockCommitinfo 2025-10-19 15:43:14 +00:00
dependabot[bot]andGitHub 7bf04e494a Bump follow-redirects from 1.15.4 to 1.15.6 (#108) 2024-03-16 22:50:14 +00:00
dependabot[bot]andGitHub ed5cfbc952 Bump es5-ext from 0.10.53 to 0.10.63 (#105) 2024-02-27 12:56:00 +00:00
dependabot[bot]andGitHub 030aeaf0ea Bump follow-redirects from 1.15.1 to 1.15.4 (#101) 2024-01-10 05:04:34 +00:00
dependabot[bot]andGitHub 440f672f9e Bump @alcalzone/release-script from 3.6.0 to 3.7.0 (#96) 2023-12-01 07:16:58 +00:00
BluefoxandGitHub 0c9b1b1ed1 Merge pull request #92 from ioBroker/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-11-06 09:37:37 +01:00
BluefoxandGitHub 0090c6cfe9 Merge pull request #94 from ioBroker/dependabot/github_actions/actions/setup-node-4
Bump actions/setup-node from 3 to 4
2023-11-06 09:32:58 +01:00
dependabot[bot]andGitHub 42100114c6 Bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 03:19:50 +00:00
dependabot[bot]andGitHub a4039286c7 Bump chai from 4.3.8 to 4.3.10 (#90) 2023-10-01 04:56:17 +00:00
dependabot[bot]andGitHub c497e40bef Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 03:15:33 +00:00
dependabot[bot]andGitHub 5573276759 Bump chai from 4.3.7 to 4.3.8 (#89) 2023-09-01 08:00:49 +00:00
dependabot[bot]andGitHub 8ad88617c0 Bump @alcalzone/release-script from 3.5.9 to 3.6.0 (#87) 2023-08-01 07:32:35 +00:00
dependabot[bot]andGitHub f7ba3f0cad Bump @alcalzone/release-script-plugin-iobroker from 3.5.9 to 3.6.0 (#85) 2023-08-01 04:45:54 +00:00
dependabot[bot]andGitHub 4721397d40 Bump word-wrap from 1.2.3 to 1.2.4 (#83) 2023-07-20 02:56:12 +00:00
BluefoxandGitHub ff423942fc Merge pull request #78 from ioBroker/dependabot/npm_and_yarn/ws-8.13.0
Bump ws from 8.12.1 to 8.13.0
2023-04-05 09:01:58 +02:00
dependabot[bot]andGitHub 7129b7f231 Bump ws from 8.12.1 to 8.13.0
Bumps [ws](https://github.com/websockets/ws) from 8.12.1 to 8.13.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.12.1...8.13.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 05:19:02 +00:00
dependabot[bot]andGitHub 97ca358ea5 Bump @iobroker/adapter-core from 2.6.7 to 2.6.8 (#77) 2023-04-01 04:50:47 +00:00
BluefoxandGitHub ddea7eb899 Merge pull request #76 from ioBrokerTranslator/weblate-adapters-hass
Translations update from ioBroker Translation System
2023-03-29 20:12:46 +02:00
diemadeandWeblate 781d103d93 Translated using Weblate (German)
Currently translated at 100.0% (6 of 6 strings)

Translation: ioBroker Adapters/hass
Translate-URL: https://weblate.iobroker.net/projects/adapters/hass/de/
2023-03-20 02:43:55 +01:00
dependabot[bot]andGitHub c211767bb3 Bump ws from 8.12.0 to 8.12.1 (#75) 2023-03-01 07:18:12 +00:00
BluefoxandGitHub c18cc4f9f8 Merge pull request #74 from ioBroker/dependabot/npm_and_yarn/ws-8.12.0
Bump ws from 8.11.0 to 8.12.0
2023-02-27 14:23:46 +01:00
dependabot[bot]andGitHub 90def09878 Bump ws from 8.11.0 to 8.12.0
Bumps [ws](https://github.com/websockets/ws) from 8.11.0 to 8.12.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.11.0...8.12.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-01 04:47:11 +00:00
Ingo Fischer 976c611f97 chore: release v1.4.0
* (Apollon77) Added more guidance logging when setting services incorrectly
* (Apollon77) Prevent crashes when attributes contain "." at the end of their names
* (Apollon77) Added logging for state updates for unknown objects
2023-01-03 22:53:58 +01:00
Ingo Fischer 3887500a62 * update license years 2023-01-03 22:53:11 +01:00
Ingo Fischer 62ef7cabf8 * detect json better 2023-01-03 00:38:58 +01:00
dependabot[bot]andGitHub 8fc6f4ac01 Bump mocha from 10.1.0 to 10.2.0 (#72) 2023-01-01 04:47:06 +00:00
Ingo Fischer 649bdfce2f * fix 2022-12-31 09:22:56 +01:00
Ingo Fischer e0fcc28746 * add translations 2022-12-22 23:29:01 +01:00
Ingo FischerandGitHub 71813c2df9 Merge pull request #71 from ioBrokerTranslator/weblate-adapters-hass
Translations update from ioBroker Translation System
2022-12-22 23:27:42 +01:00
mcm1957andWeblate ac8d3617b0 Translated using Weblate (Spanish)
Currently translated at 0.0% (0 of 6 strings)

Translation: ioBroker Adapters/hass
Translate-URL: https://weblate.iobroker.net/projects/adapters/hass/es/
2022-12-22 21:28:58 +01:00
Ingo Fischer 2986071d2e * update readme 2022-12-21 20:34:47 +01:00
Ingo Fischer 56a9cbbb1c * (Apollon77) Prevent crashes when attributes contain "." at the end of their names
* (Apollon77) Added logging for state updates for unknown objects
2022-12-08 23:31:18 +01:00
Ingo FischerandGitHub 5a8e22b408 Merge pull request #70 from ioBroker/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2022-12-08 23:12:34 +01:00
dependabot[bot]andGitHub 1c46a7772f Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-08 22:09:10 +00:00
Ingo Fischer 659152be2e * add more guidance on how to call 2022-12-08 23:08:37 +01:00
Ingo Fischer ad8f360c17 * add adapter-dev, remove gulp
* add more guidance on how to call
* update deps
2022-12-08 23:06:36 +01:00
dependabot[bot]andGitHub 88d027d575 Update axios requirement from ^1.1.3 to ^1.2.0 (#67) 2022-12-01 11:50:51 +00:00
dependabot[bot]andGitHub 9727116d46 Update chai requirement from ^4.3.6 to ^4.3.7 (#66) 2022-12-01 08:12:36 +00:00
BluefoxandGitHub 03fba79027 Merge pull request #60 from ioBroker/dependabot/npm_and_yarn/ws-tw-8.10.0
Update ws requirement from ^8.8.1 to ^8.10.0
2022-12-01 01:42:19 +01:00
BluefoxandGitHub 50fd571f0b Merge pull request #63 from ioBroker/dependabot/npm_and_yarn/axios-1.1.3
Bump axios from 0.27.2 to 1.1.3
2022-12-01 01:42:04 +01:00
BluefoxandGitHub 39bd989a41 Merge pull request #64 from lgtm-migrator/codeql
Add CodeQL workflow for GitHub code scanning
2022-12-01 01:41:54 +01:00
LGTM Migrator 407b5bfb07 Add CodeQL workflow for GitHub code scanning 2022-11-10 18:24:33 +00:00
dependabot[bot]andGitHub cdc3aba0b2 Update ws requirement from ^8.8.1 to ^8.10.0
Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.8.1...8.10.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 09:23:11 +00:00
dependabot[bot]andGitHub 8a2933411f Update @iobroker/adapter-core requirement from ^2.6.6 to ^2.6.7 (#62) 2022-11-01 09:21:45 +00:00
dependabot[bot]andGitHub 68c6bd989f Bump axios from 0.27.2 to 1.1.3
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 1.1.3.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v1.1.3)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 06:06:26 +00:00
dependabot[bot]andGitHub e03a2cfc80 Update mocha requirement from ^10.0.0 to ^10.1.0 (#61) 2022-11-01 06:04:19 +00:00
dependabot[bot]andGitHub 9558c39deb Update @iobroker/adapter-core requirement from ^2.6.0 to ^2.6.6 (#58) 2022-10-01 04:01:09 +00:00
dependabot[bot]andGitHub 2956ee506a Update ws requirement from ^8.8.0 to ^8.8.1 (#57) 2022-08-01 03:13:53 +00:00
Ingo Fischer 62ce173d22 chore: release v1.3.0
* (Apollon77) Further optimize sending data to HASS and allow to set values like numbers as normal states if the service has one attribute and it can be mapped
2022-07-01 10:10:38 +02:00
Ingo Fischer 45b0550e0c * add logging 2022-06-30 17:42:24 +02:00
Ingo Fischer 0ac6c66aaf * (Apollon77) Further optimize sending data to HASS and allow to set values like numbers as normal states if the service has one attribute and it can be mapped 2022-06-30 11:51:23 +02:00
Ingo Fischer b54916d743 chore: release v1.2.0
* (Apollon77) IMPORTANT: Replace special characters in entity attribute names with an underscore! Object IDs might change!
* (Apollon77) make sure a "null" value in state changes is not crashing
2022-06-17 23:23:11 +02:00
Ingo Fischer 52d8bddb11 * (Apollon77) make sure a "null" value in state changes is not crashing 2022-06-17 23:20:26 +02:00
dependabot[bot]andGitHub 271bd03d14 Update @alcalzone/release-script requirement from ^3.5.8 to ^3.5.9 (#53) 2022-06-01 09:45:24 +00:00
dependabot[bot]andGitHub 36bc7c99fd Update @alcalzone/release-script-plugin-license requirement (#51) 2022-06-01 07:48:32 +00:00
dependabot[bot]andGitHub cc76a49d62 Update @alcalzone/release-script-plugin-iobroker requirement (#54) 2022-06-01 07:05:24 +00:00
dependabot[bot]andGitHub 7a2475ba02 Update @alcalzone/release-script-plugin-license requirement (#47) 2022-05-01 14:15:54 +00:00
dependabot[bot]andGitHub 47cb4cfad8 Update @alcalzone/release-script requirement from ^3.5.6 to ^3.5.8 (#49) 2022-05-01 11:54:23 +00:00
dependabot[bot]andGitHub 2a473f88b0 Update @alcalzone/release-script-plugin-iobroker requirement (#48) 2022-05-01 09:16:22 +00:00
dependabot[bot]andGitHub 245032a178 Bump axios from 0.26.1 to 0.27.2 (#46) 2022-05-01 05:37:18 +00:00
BluefoxandGitHub 96e6c5b471 Merge pull request #45 from ioBroker/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2022-04-03 14:39:36 +02:00
dependabot[bot]andGitHub 82614cdad0 Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-01 04:20:10 +00:00
Ingo Fischer e6b58530d4 chore: release v1.1.2
* (Apollon77) Fix crash cases reported by Sentry
2022-03-29 09:07:54 +02:00
Ingo Fischer dc47f48004 * (Apollon77) Fix crash cases reported by Sentry (IOBROKER-HASS-2) 2022-03-29 08:59:10 +02:00
Ingo Fischer 4096826099 chore: release v1.1.1
* (Apollon77) Show password fields masked again in config
2022-03-25 09:25:45 +01:00
Ingo Fischer 3021e3f5a5 * (Apollon77) Show password fields masked again in config 2022-03-25 09:25:32 +01:00
Ingo Fischer a4a7abedbe chore: release v1.1.0
* IMPORTANT: You need to re-enter the password once after installing this version!
* (Apollon77) Implement Service triggers to use any value to trigger or stringified JSON to call with fields
* (Apollon77) Optimize unload handling
* (Apollon7) Add Sentry for crash reporting
2022-03-24 21:33:49 +01:00
Ingo Fischer 9ab4db078f * add message for admin to io-package 2022-03-24 21:33:35 +01:00
Ingo Fischer 2ec6bf8150 * add documentation on how to call services 2022-03-24 18:31:15 +01:00
Ingo Fischer 0dd97e69c8 * update ui and translations 2022-03-23 10:28:24 +01:00
Ingo Fischer 8ff8341088 * update ui and translations 2022-03-23 08:23:24 +01:00
Ingo Fischer 0cb8ed8a13 * update ui and translations 2022-03-23 08:23:16 +01:00
Ingo Fischer 46d58e00e3 * rework index_m 2022-03-23 08:07:07 +01:00
Ingo Fischer 0fa719edd0 * rework index_m 2022-03-23 08:04:50 +01:00
Ingo Fischer 5cb493481e * IMPORTANT: You need to re-enter the password once after installing this version!
* rework index_m
2022-03-22 22:57:40 +01:00
Ingo Fischer 0d28c7e54b * revert 2022-03-22 17:18:29 +01:00
Ingo Fischer d89288a0b8 * ignore ignored services also when changes come in 2022-03-22 15:15:04 +01:00
Ingo Fischer f03bf3d9a6 * only process state changes tat are not null 2022-03-21 22:59:35 +01:00
Ingo Fischer 3b83e686f7 * (Apollon77) Implement Service triggers to use any value to trigger or stringified JSON to call with fields 2022-03-21 17:11:48 +01:00
BluefoxandGitHub b887a75ede Merge pull request #38 from ioBroker/dependabot/github_actions/actions/setup-node-3
Bump actions/setup-node from 2.5.1 to 3
2022-03-07 18:45:58 +01:00
BluefoxandGitHub 96bc8592b3 Merge pull request #42 from ioBroker/dependabot/npm_and_yarn/ws-tw-8.5.0
Update ws requirement from ^8.2.3 to ^8.5.0
2022-03-07 18:45:47 +01:00
BluefoxandGitHub 4d939862d6 Merge branch 'master' into dependabot/npm_and_yarn/ws-tw-8.5.0 2022-03-07 18:45:37 +01:00
BluefoxandGitHub d297bf3d27 Merge pull request #41 from ioBroker/dependabot/npm_and_yarn/iobroker/adapter-core-tw-2.6.0
Update @iobroker/adapter-core requirement from ^2.5.1 to ^2.6.0
2022-03-07 18:45:17 +01:00
dependabot[bot]andGitHub ba6f42601a Update mocha requirement from ^9.2.0 to ^9.2.1 (#40) 2022-03-01 09:22:32 +00:00
dependabot[bot]andGitHub 46eebb0a82 Update ws requirement from ^8.2.3 to ^8.5.0
Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.2.3...8.5.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 05:59:32 +00:00
dependabot[bot]andGitHub 3ffe4fe555 Update @iobroker/adapter-core requirement from ^2.5.1 to ^2.6.0
Updates the requirements on [@iobroker/adapter-core](https://github.com/ioBroker/adapter-core) to permit the latest version.
- [Release notes](https://github.com/ioBroker/adapter-core/releases)
- [Commits](https://github.com/ioBroker/adapter-core/compare/v2.5.1...v2.6.0)

---
updated-dependencies:
- dependency-name: "@iobroker/adapter-core"
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 05:59:26 +00:00
dependabot[bot]andGitHub 50962fb833 Bump actions/setup-node from 2.5.1 to 3
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.5.1 to 3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.5.1...v3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 04:21:24 +00:00
dependabot[bot]andGitHub aa7a06d750 Update chai requirement from ^4.3.4 to ^4.3.6 (#35) 2022-02-01 18:00:09 +00:00
dependabot[bot]andGitHub 7c966c5bd9 Update mocha requirement from ^9.1.3 to ^9.2.0 (#34) 2022-02-01 05:03:43 +00:00
Ingo Fischer 1f1fc7ef43 update testAdapter.js 2022-01-29 16:08:30 +01:00
Ingo Fischer 4e19c7e334 add .mocharc.json 2022-01-29 15:27:39 +01:00
Ingo Fischer 84dee7cd6a add mocha.setup.js 2022-01-29 15:27:39 +01:00
Ingo Fischer 5d8816f464 update testAdapter.js 2022-01-29 15:27:39 +01:00
Ingo Fischer f1376e5e5b update testing setup.js 2022-01-29 15:27:38 +01:00
dependabot[bot]andGitHub d042d65588 Bump actions/setup-node from 2.5.0 to 2.5.1 (#29) 2022-01-01 04:27:42 +00:00
BluefoxandGitHub 088113848d Merge pull request #28 from ioBroker/dependabot/github_actions/actions/setup-node-2.5.0
Bump actions/setup-node from 2.4.1 to 2.5.0
2021-12-14 13:15:57 +01:00
dependabot[bot]andGitHub c8be38859d Bump actions/setup-node from 2.4.1 to 2.5.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.4.1...v2.5.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 03:05:45 +00:00
dependabot[bot]andGitHub fd5517c976 Update mocha requirement from ^9.1.2 to ^9.1.3 (#24) 2021-11-01 06:03:44 +00:00
dependabot[bot]andGitHub aedde9a4b9 Update ws requirement from ^8.2.2 to ^8.2.3 (#23) 2021-11-01 05:35:58 +00:00
BluefoxandGitHub a148fa4e33 Merge pull request #22 from ioBroker/dependabot/npm_and_yarn/mocha-tw-9.1.2
Update mocha requirement from ^9.1.1 to ^9.1.2
2021-10-05 18:42:21 +02:00
BluefoxandGitHub 1451e497d8 Merge pull request #21 from ioBroker/dependabot/npm_and_yarn/ws-tw-8.2.2
Update ws requirement from ^8.2.1 to ^8.2.2
2021-10-05 18:42:12 +02:00
dependabot[bot]andGitHub 85ef3afc04 Bump actions/setup-node from 2.4.0 to 2.4.1 (#19) 2021-10-01 03:36:34 +00:00
dependabot[bot]andGitHub a68f254e60 Update mocha requirement from ^9.1.1 to ^9.1.2
Updates the requirements on [mocha](https://github.com/mochajs/mocha) to permit the latest version.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v9.1.1...v9.1.2)

---
updated-dependencies:
- dependency-name: mocha
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-01 03:10:27 +00:00
dependabot[bot]andGitHub a75fbf84bb Update ws requirement from ^8.2.1 to ^8.2.2
Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.2.1...8.2.2)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-01 03:10:10 +00:00
Ingo Fischer 66953a6e0d update testing setup.js 2021-09-05 00:09:57 +02:00
52 changed files with 12021 additions and 2429 deletions
+25 -12
View File
@@ -1,16 +1,29 @@
# Dependabot configuration
# Cooldown delays updating normal npm dependencies by 7 days but allows security updates to be processed immediately.
# Note: Cooldown is not supported for the github-actions ecosystem.
# Reference: https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: monthly
time: "04:00"
timezone: Europe/Berlin
- package-ecosystem: npm
directory: "/"
interval: 'cron'
timezone: 'Europe/Berlin'
cronjob: '15 2 17 * *'
open-pull-requests-limit: 15
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: monthly
time: "04:00"
timezone: Europe/Berlin
open-pull-requests-limit: 20
versioning-strategy: increase
interval: 'cron'
timezone: 'Europe/Berlin'
cronjob: '15 2 17 * *'
open-pull-requests-limit: 15
versioning-strategy: 'increase'
cooldown:
default-days: 7
ignore:
- dependency-name: '@types/node'
update-types:
- 'version-update:semver-major'
@@ -0,0 +1,35 @@
# Workflow for auto-merging Dependabot PRs
# This workflow uses the action-automerge-dependabot action to automatically merge
# Dependabot PRs based on the rules defined in .github/auto-merge.yml
name: Auto-Merge Dependabot PRs
on:
# Trigger when a PR is opened or updated
# WARNING: This needs to be run in the PR base, DO NOT build untrusted code in this action
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
auto-merge:
runs-on: ubuntu-latest
# Only run if actor is dependabot
if: github.actor == 'dependabot[bot]'
permissions:
contents: write
pull-requests: write
steps:
- name: Auto-merge Dependabot PRs
uses: iobroker-bot-orga/action-automerge-dependabot@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Optional: Path to your auto-merge configuration file
# config-file-path: '.github/auto-merge.yml'
# Optional: Merge method (merge, squash, or rebase)
# merge-method: 'squash'
# Optional: Wait for other checks to complete
# wait-for-checks: 'true'
# Optional: Maximum time to wait for checks in seconds (default: 3600)
# max-wait-time: '3600'
+41
View File
@@ -0,0 +1,41 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: "4 5 * * 5"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ javascript ]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check if PR should be auto-merged
uses: ahmadnassri/action-dependabot-auto-merge@v2
+29 -115
View File
@@ -1,6 +1,3 @@
# This is a composition of lint and test scripts
# Make sure to update this file along with the others
name: Test and Release
# Run this job on all pushes and pull requests
@@ -8,14 +5,19 @@ name: Test and Release
on:
push:
branches:
- '*'
- "master"
tags:
# normal versions
- "v?[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+"
# pre-releases
- "v?[0-9]+.[0-9]+.[0-9]+-**"
- "v[0-9]+.[0-9]+.[0-9]+-**"
pull_request: {}
# Cancel previous PR/branch runs when a new commit is pushed
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
# Performs quick checks before the expensive test runs
check-and-lint:
@@ -23,136 +25,48 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.4.0
- uses: ioBroker/testing-action-check@v1
with:
node-version: ${{ matrix.node-version }}
node-version: '24.x'
lint: true
- name: Install Dependencies
run: npm install
# - name: Perform a type check
# run: npm run check:ts
# env:
# CI: true
# - name: Lint TypeScript code
# run: npm run lint
# - name: Test package files
# run: npm run test:package
# Runs adapter tests on all supported node versions and OSes
adapter-tests:
if: contains(github.event.head_commit.message, '[skip ci]') == false
needs: [check-and-lint]
if: contains(github.event.head_commit.message, '[skip ci]') == false
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [20.x, 22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.4.0
- uses: ioBroker/testing-action-adapter@v1
with:
node-version: ${{ matrix.node-version }}
os: ${{ matrix.os }}
build: true
- name: Install Dependencies
run: npm install
- name: Run local tests
run: npm test
# - name: Run unit tests
# run: npm run test:unit
# - name: Run integration tests # (linux/osx)
# if: startsWith(runner.OS, 'windows') == false
# run: DEBUG=testing:* npm run test:integration
# - name: Run integration tests # (windows)
# if: startsWith(runner.OS, 'windows')
# run: set DEBUG=testing:* & npm run test:integration
# Deploys the final package to NPM
deploy:
needs: [adapter-tests]
needs: [check-and-lint, adapter-tests]
# Permissions are required to create GitHub releases and npm trusted publishing
permissions:
contents: write
id-token: write
# Trigger this step only when a commit on master is tagged with a version number
if: |
contains(github.event.head_commit.message, '[skip ci]') == false &&
github.event_name == 'push' &&
startsWith(github.ref, 'refs/tags/')
startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.4.0
- uses: ioBroker/testing-action-deploy@v1
with:
node-version: ${{ matrix.node-version }}
- name: Extract the version and commit body from the tag
id: extract_release
# The body may be multiline, therefore we need to escape some characters
run: |
VERSION="${{ github.ref }}"
VERSION=${VERSION##*/}
VERSION=${VERSION##*v}
echo "::set-output name=VERSION::$VERSION"
BODY=$(git show -s --format=%b)
BODY="${BODY//'%'/'%25'}"
BODY="${BODY//$'\n'/'%0A'}"
BODY="${BODY//$'\r'/'%0D'}"
echo "::set-output name=BODY::$BODY"
- name: Install Dependencies
run: npm install
# - name: Create a clean build
# run: npm run build
- name: Publish package to npm
run: |
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami
npm publish
- name: Create Github Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release v${{ steps.extract_release.outputs.VERSION }}
draft: false
# Prerelease versions create prereleases on Github
prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }}
body: ${{ steps.extract_release.outputs.BODY }}
#- name: Notify Sentry.io about the release
# run: |
# npm i -g @sentry/cli
# export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
# export SENTRY_URL=https://sentry.iobroker.net
# export SENTRY_ORG=iobroker
# export SENTRY_PROJECT=iobroker-hass
# export SENTRY_VERSION=iobroker.hass@${{ steps.extract_release.outputs.VERSION }}
# sentry-cli releases new $SENTRY_VERSION
# sentry-cli releases finalize $SENTRY_VERSION
# Add the following line BEFORE finalize if repositories are connected in Sentry
# sentry-cli releases set-commits $SENTRY_VERSION --auto
# Add the following line BEFORE finalize if sourcemap uploads are needed
# sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/
node-version: '24.x'
github-token: ${{ secrets.GITHUB_TOKEN }}
build: true
+8 -1
View File
@@ -4,4 +4,11 @@ tmp
admin/i18n/flat.txt
admin/i18n/*/flat.txt
iob_npm.done
package-lock.json
#ignore .commitinfo created by ioBroker release script
.commitinfo
.claude/settings.local.json
# ioBroker dev-server
.dev-server/
-12
View File
@@ -1,12 +0,0 @@
/**/*
/gulpfile.js
gulpfile.js
!/admin/**/*
!/admin/*
!/lib/**/*
!/lib/*
!/io-package.json
!/package.json
!/LICENSE
!/main.js
!/README.md
+6
View File
@@ -0,0 +1,6 @@
{
"plugins": ["iobroker", "license"],
"exec": {
"before_commit": "npm run build"
}
}
+27
View File
@@ -0,0 +1,27 @@
# Older changes
## 1.2.0 (2022-06-17)
* (Apollon77) IMPORTANT: Replace special characters in entity attribute names with an underscore! Object IDs might change!
* (Apollon77) make sure a "null" value in state changes is not crashing
## 1.1.2 (2022-03-29)
* (Apollon77) Fix crash cases reported by Sentry
## 1.1.1 (2022-03-25)
* (Apollon77) Show password fields masked again in config
## 1.1.0 (2022-03-24)
* IMPORTANT: You need to re-enter the password once after installing this version!
* (Apollon77) Implement Service triggers to use any value to trigger or stringified JSON to call with fields
* (Apollon77) Optimize unload handling
* (Apollon7) Add Sentry for crash reporting
## 1.0.1 (2021-09-04)
* IMPORTANT: js-controller 2.0 is needed at least!
* (Apollon77) Fix start issue
* (Apollon77/Garfonso) Fix issue where value could not be set in hass
## 1.0.0 (2020-12-13)
* (bluefox) added the support of compact mode
## 0.1.0
* (bluefox) initial release
+66
View File
@@ -0,0 +1,66 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ioBroker.hass is an ioBroker adapter that connects Home Assistant to ioBroker via WebSocket API. It reads Home Assistant entities/services and exposes them as ioBroker objects, and forwards ioBroker state changes back as Home Assistant service calls.
## Commands
```bash
# Build TypeScript
npm run build
# Run all tests (Mocha)
npm test
# Run only package validation tests
npm run test:package
# Lint
npm run lint
# Format check
npx prettier --check .
# Translate adapter strings
npm run translate
# Release (patch/minor/major)
npm run release-patch
npm run release-minor
npm run release-major
```
## Architecture
TypeScript class-based adapter. Source in `src/`, compiled output in `build/`.
- **src/main.ts** — `HassAdapter` class extending `Adapter`. Daemon mode entry point. Handles:
- Connecting to Home Assistant and syncing entities/services into ioBroker objects
- `parseStates()` — maps HASS entities, attributes, and services to ioBroker channels/states
- `onStateChange()` — converts ioBroker commands (`ack=false`) into HASS `callService()` calls, supporting both direct values (single-field services) and JSON-stringified objects (multi-field services)
- Object/state synchronization via `syncObjects`/`syncStates`
- **src/lib/hass.ts** — `HASS` class extending `EventEmitter`. WebSocket client for Home Assistant with:
- Auto-reconnect (3s delay)
- Message ID tracking for request-response correlation
- Methods: `getConfig`, `getStates`, `getServices`, `getPanels`, `callService`
- Events: `connected`, `disconnected`, `error`, `state_changed`
The codebase uses **callback-based async** (no promises/async-await).
## Testing
Tests use Mocha with two ioBroker-specific frameworks:
- `@iobroker/legacy-testing` — spins up a js-controller instance for integration tests (`test/testAdapter.js`)
- `@iobroker/testing` — validates package.json and io-package.json structure (`test/testPackageFiles.js`)
## Configuration
Adapter config (defined in `io-package.json`): `host`, `port`, `password` (long-lived access token), `secure` (boolean for wss).
## CI
GitHub Actions runs lint on Node 22, adapter tests on Node 20/22/24 across Linux/Windows/macOS. Deploys to npm on semantic version tags via OIDC trusted publishing.
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018-2020 bluefox <dogafox@gmail.com>
Copyright (c) 2018-2026 bluefox <dogafox@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+135 -14
View File
@@ -1,44 +1,165 @@
![Logo](admin/hass.png)
# ioBroker.hass
![Number of Installations](http://iobroker.live/badges/hass-installed.svg)
![Number of Installations](http://iobroker.live/badges/hass-stable.svg)
[![NPM version](http://img.shields.io/npm/v/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass)
![Test and Release](https://github.com/ioBroker/ioBroker.hass/workflows/Test%20and%20Release/badge.svg)
[![Translation status](https://weblate.iobroker.net/widgets/adapters/-/hass/svg-badge.svg)](https://weblate.iobroker.net/engage/adapters/?utm_source=widget)
[![Downloads](https://img.shields.io/npm/dm/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass)
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information on how to disable the error reporting, see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0.
This adapter allows the connecting of Home Assistant to ioBroker.
## Usage
Create a long term token in HASS and use it as PW (copy it also in the repeat field).
Create a long-term token in HASS and use it as PW (copy it also in the repeat field).
Then it should read out all attributes for all devices. Services might be controllable (e.g. "turn_on"). To control services, you have two options:
### Set a direct value
Set the state with an ack=false value which is not a string (e.g. Boolean true), then it will be triggered also in HASS without additional service data. This will only work if the service has one field to be sent - then the value is sent as this field! If the service has more than one field, you will find a warning in the log that provides more details about the fields that are possible to be sent, e.g.
```
Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!
Allowed field keys are: temperature, target_temp_high, target_temp_low, hvac_mode
```
### Set a stringified JSON to provide one or multiple fields
Set the state with an ack=false String value which is a stringified JSON object to call the service and use the JSON object as service data
For the last option on a light.turn_off with e.g. `{"transition":10,"flash":"short"}` these two service data details are sent with the call to HASS. The available fields with their exact data definition can be seen in the JSON definition of the ioBroker object in the `native` fields section and would look like the following in the above example:
```json5
{
// ...
native: {
"fields": {
"transition": {
"name": "Transition",
"description": "Duration it takes to get to next state.",
"selector": {
"number": {
"min": 0,
"max": 300,
"unit_of_measurement": "seconds"
}
}
},
"flash": {
"name": "Flash",
"description": "If the light should flash.",
"advanced": true,
"selector": {
"select": {
"options": [
"long",
"short"
]
}
}
}
},
"entity_id": "light.mi_control_hub_light",
"attr": "turn_off",
"type": "light"
}
//...
}
```
For some services like set_speed it is required to call with a JSON object like `{speed: "high"}` in general to provide required values. In this case the field definition looks e.g. like:
```json5
{
//...
native: {
"fields": {
"speed": {
"name": "Speed",
"description": "Speed setting.",
"required": true,
"example": "low",
"selector": {
"text": null
}
}
}
// ...
}
// ...
}
```
## Configuration
There is a good article about connection.
There is a good article about the connection.
Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen/
**Unfortunately only in german, but the [google translate works rather good](https://translate.google.com/translate?hl=en&sl=de&tl=en&u=https%3A%2F%2Fwww.smarthomejetzt.de%2Fmit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen%2F)**
**Unfortunately only in German, but the [Google Translate works rather good](https://translate.google.com/translate?hl=en&sl=de&tl=en&u=https%3A%2F%2Fwww.smarthomejetzt.de%2Fmit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen%2F)**
## Entity exclude filter
Optionally, restrict which Home Assistant entities are synchronised into ioBroker.
Each non-empty, non-comment line in the **Exclude patterns** field is a glob
(only `*` is a wildcard and matches any sequence of characters, including `.`).
Patterns are matched case-sensitively against the full `entity_id` (e.g.
`switch.living_room`). An entity that matches any pattern is:
- skipped when objects are created or updated (initial sync and re-syncs)
- ignored when its state changes in HASS (no state writes triggered in ioBroker)
Lines starting with `#` are treated as comments.
Examples:
```
# Drop every entity whose name starts with `iob_`, regardless of domain:
*.iob_*
# Drop sensors only:
sensor.iob_*
```
Tick **Verbose filter logging** to log every excluded `entity_id` individually
during the first sync (requires adapter loglevel `info` or `debug`). Subsequent
re-syncs only emit the aggregate count to keep the log clean.
An empty pattern list leaves the adapter behaviour identical to previous versions.
<!--
Placeholder for the next version (at the beginning of the line):
### __WORK IN PROGRESS__
### **WORK IN PROGRESS**
-->
## Changelog
### 2.1.0 (2026-05-16)
* (mokusone) Added optional entity exclude filter with glob patterns, configurable via the admin UI, plus a verbose-logging toggle for inspecting matches
* (@klein0r) Use `/core/` instead of `/api/` when connecting to supervisor directly (e.g., in ha app)
* (@klein0r) Use ENV var SUPERVISOR_TOKEN as fallback for password
### 1.0.1 (2021-09-04)
* IMPORTANT: js-controller 2.0 is needed st least!
* (Apollon77) Fix start issue
* (Apollon77/Garfonso) Fix issue where value could not be set in hass
### 2.0.4 (2026-05-05)
* (@GermanBluefox) Tried to keep the custom settings of the objects when updating them with new data from HASS
### 1.0.0 (2020-12-13)
* (bluefox) added the support of compact mode
### 2.0.3 (2026-04-02)
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
* (@Titanium177) Added roles for states and added debouncing for reading states from hass
### 0.1.0
* (bluefox) initial release
### 1.4.0 (2023-01-03)
* (Apollon77) Added more guidance logging when setting services incorrectly
* (Apollon77) Prevent crashes when attributes contain "." at the end of their names
* (Apollon77) Added logging for state updates for unknown objects
## License
### 1.3.0 (2022-07-01)
* (Apollon77) Further optimize sending data to HASS and allow setting values like numbers as normal states if the service has one attribute and it can be mapped
[Older changelogs can be found there](CHANGELOG_OLD.md)## License
The MIT License (MIT)
Copyright (c) 2018-2020 bluefox <dogafox@gmail.com>
Copyright (c) 2018-2026 bluefox <dogafox@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Ausgefilterte Entitäten beim Adapter-Start löschen",
"Exclude patterns (one per line)": "Filter-Patterns (eines pro Zeile)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-Patterns mit * als Wildcard. Zeilen mit # sind Kommentare. Leer = Filter aus.",
"Home assistant IP": "IP-Adresse von Home Assistant",
"Home assistant WS Port": "Home Assistant WebSocket-Port",
"Password": "Passwort",
"Password repeat": "Passwort wiederholen",
"Passwords missmatch!": "Passwörter stimmen nicht überein!",
"Secure": "HTTPS?",
"Verbose filter logging": "Ausführliches Filter-Logging",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Wenn aktiv: jede ausgeschlossene entity_id wird beim initialen Adapter-Sync einzeln geloggt (benötigt Loglevel info oder debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Wenn aktiv: bei jedem Adapter-Start werden vorhandene Objekte unter entities.*, deren entity_id auf die Filter-Patterns matched, gelöscht. Objekte mit Custom-Adapter-Konfiguration (history/influxdb/sql) bleiben erhalten und werden geloggt."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start",
"Exclude patterns (one per line)": "Exclude patterns (one per line)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.",
"Home assistant IP": "Home assistant IP",
"Home assistant WS Port": "Home assistant WS Port",
"Password": "Password",
"Password repeat": "Password repeat",
"Passwords missmatch!": "Passwords missmatch!",
"Secure": "HTTPS?",
"Verbose filter logging": "Verbose filter logging",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Limpiar las entidades excluidas al iniciar el adaptador.",
"Exclude patterns (one per line)": "Patrones de exclusión (uno por línea)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Patrones glob, * como comodín. Las líneas que empiezan con # son comentarios. Vacío = filtro desactivado.",
"Home assistant IP": "IP del asistente de hogar",
"Home assistant WS Port": "Asistente doméstico Puerto WS",
"Password": "Clave",
"Password repeat": "Repite la contraseña",
"Passwords missmatch!": "¡Las contraseñas no coinciden!",
"Secure": "¿HTTPS?",
"Verbose filter logging": "Registro detallado del filtro",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Cuando está activado, cada entity_id excluido se registra individualmente durante la sincronización inicial del adaptador (requiere loglevel info o debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Cuando está habilitada, en cada inicio del adaptador se eliminan los objetos existentes en entities.* cuyo entity_id coincida con los patrones de exclusión. Los objetos con configuración de adaptador personalizada (history/influxdb/sql) se conservan y se registran."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Nettoyage des entités exclues au démarrage de l'adaptateur",
"Exclude patterns (one per line)": "Modèles d'exclusion (un par ligne)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Modèles glob, * comme joker. Les lignes commençant par # sont des commentaires. Vide = filtre désactivé.",
"Home assistant IP": "IP de l'assistant à domicile ",
"Home assistant WS Port": "Port WS de l'assistant domestique ",
"Password": "Mot de passe",
"Password repeat": "Répéter le mot de passe ",
"Passwords missmatch!": "Les mots de passe ne correspondent pas !",
"Secure": "HTTPS ?",
"Verbose filter logging": "Journalisation détaillée du filtre",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Lorsqu'activé, chaque entity_id exclu est journalisé individuellement pendant la synchronisation initiale de l'adaptateur (nécessite loglevel info ou debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Lorsqu'elle est activée, cette option supprime, à chaque démarrage de l'adaptateur, les objets existants sous entities.* dont l'entity_id correspond aux critères d'exclusion. Les objets avec une configuration d'adaptateur personnalisée (history/influxdb/sql) sont conservés et consignés."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Pulizia delle entità escluse all'avvio dell'adattatore",
"Exclude patterns (one per line)": "Pattern di esclusione (uno per riga)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Pattern glob, * come wildcard. Le righe che iniziano con # sono commenti. Vuoto = filtro disattivato.",
"Home assistant IP": "IP assistente domestico",
"Home assistant WS Port": "Porta WS dell'assistente domestico",
"Password": "Parola d'ordine",
"Password repeat": "Ripeti password",
"Passwords missmatch!": "Le password non corrispondono!",
"Secure": "HTTPS?",
"Verbose filter logging": "Logging dettagliato del filtro",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Se attivato, registra ogni entity_id escluso individualmente durante la sincronizzazione iniziale dell'adattatore (richiede loglevel info o debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Quando abilitata, all'avvio di ogni adattatore vengono eliminati gli oggetti esistenti in entities.* il cui entity_id corrisponde ai modelli di esclusione. Gli oggetti con configurazione personalizzata dell'adattatore (history/influxdb/sql) vengono mantenuti e registrati."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Verwijder uitgesloten entiteiten bij het opstarten van de adapter.",
"Exclude patterns (one per line)": "Uitsluitpatronen (één per regel)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-patronen, * als wildcard. Regels die beginnen met # zijn commentaar. Leeg = filter uit.",
"Home assistant IP": "Thuisassistent IP",
"Home assistant WS Port": "Thuisassistent WS Poort",
"Password": "Wachtwoord",
"Password repeat": "Wachtwoord herhalen",
"Passwords missmatch!": "Wachtwoorden komen niet overeen!",
"Secure": "HTTPS?",
"Verbose filter logging": "Uitgebreide filterlogging",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Indien ingeschakeld, wordt elke uitgesloten entity_id afzonderlijk gelogd tijdens de initiële adapter-synchronisatie (vereist loglevel info of debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Indien ingeschakeld, worden bij elke adapterstart bestaande objecten onder entities.* verwijderd waarvan de entity_id overeenkomt met de uitsluitingspatronen. Objecten met een aangepaste adapterconfiguratie (history/influxdb/sql) worden behouden en gelogd."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Wyczyść wykluczone jednostki podczas uruchamiania adaptera",
"Exclude patterns (one per line)": "Wzorce wykluczeń (jeden na linię)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Wzorce glob, * jako symbol wieloznaczny. Linie zaczynające się od # to komentarze. Puste = filtr wyłączony.",
"Home assistant IP": "Adres IP asystenta domowego",
"Home assistant WS Port": "Asystent domowy Port WS",
"Password": "Hasło",
"Password repeat": "Powtórz hasło",
"Passwords missmatch!": "Niezgodność haseł!",
"Secure": "HTTPS?",
"Verbose filter logging": "Szczegółowe logowanie filtra",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Gdy włączone, każde wykluczone entity_id jest logowane osobno podczas początkowej synchronizacji adaptera (wymaga poziomu logowania info lub debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Po włączeniu, przy każdym uruchomieniu adaptera usuwane są istniejące obiekty w encji.*, których identyfikator encji pasuje do wzorców wykluczeń. Obiekty z niestandardową konfiguracją adaptera (history/influxdb/sql) są zachowywane i rejestrowane."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Limpeza de entidades excluídas na inicialização do adaptador",
"Exclude patterns (one per line)": "Padrões de exclusão (um por linha)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Padrões glob, * como wildcard. Linhas começando com # são comentários. Vazio = filtro desligado.",
"Home assistant IP": "IP do assistente doméstico",
"Home assistant WS Port": "Assistente doméstico Porta WS",
"Password": "Senha",
"Password repeat": "Repetição de senha",
"Passwords missmatch!": "As senhas não correspondem!",
"Secure": "HTTPS?",
"Verbose filter logging": "Registro detalhado do filtro",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Quando ativado, cada entity_id excluído é registrado individualmente durante a sincronização inicial do adaptador (requer loglevel info ou debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Quando ativado, a cada inicialização do adaptador, os objetos existentes em entities.* cujo entity_id corresponde aos padrões de exclusão são excluídos. Os objetos com configuração de adaptador personalizada (history/influxdb/sql) são mantidos e registrados."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Удалите исключенные объекты при запуске адаптера.",
"Exclude patterns (one per line)": "Шаблоны исключения (по одному на строку)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблоны, * — подстановочный знак. Строки, начинающиеся с #, являются комментариями. Пусто = фильтр выключен.",
"Home assistant IP": "Домашний помощник IP",
"Home assistant WS Port": "Домашний помощник WS Порт",
"Password": "Пароль",
"Password repeat": "Повтор пароля",
"Passwords missmatch!": "Пароли не совпадают!",
"Secure": "HTTPS?",
"Verbose filter logging": "Подробное логирование фильтра",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Когда включено, каждый исключённый entity_id логируется отдельно во время начальной синхронизации адаптера (требуется loglevel info или debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "При включении этой функции при каждом запуске адаптера удалять существующие объекты в папке entities.*, чей entity_id соответствует шаблонам исключения. Объекты с пользовательской конфигурацией адаптера (history/influxdb/sql) сохраняются и записываются в журнал."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "Очищення виключених об'єктів під час запуску адаптера",
"Exclude patterns (one per line)": "Шаблони виключення (по одному в рядку)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблони, * — підстановочний знак. Рядки, що починаються з #, є коментарями. Порожньо = фільтр вимкнено.",
"Home assistant IP": "Домашній помічник IP",
"Home assistant WS Port": "Домашній помічник WS Порт",
"Password": "Пароль",
"Password repeat": "Повторення пароля",
"Passwords missmatch!": "Паролі не збігаються!",
"Secure": "HTTPS?",
"Verbose filter logging": "Детальне логування фільтра",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Коли увімкнено, кожен виключений entity_id логується окремо під час початкової синхронізації адаптера (потрібен loglevel info або debug).",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Якщо ввімкнено, на кожному адаптері починається видалення існуючих об'єктів у розділі entity.*, чий entity_id відповідає шаблонам виключення. Об'єкти з власною конфігурацією адаптера (history/influxdb/sql) зберігаються та реєструються."
}
+14
View File
@@ -0,0 +1,14 @@
{
"Cleanup excluded entities on adapter start": "适配器启动时清理排除的实体",
"Exclude patterns (one per line)": "排除模式(每行一个)",
"Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob 模式,* 通配符。以 # 开头的行是注释。为空 = 过滤器关闭。",
"Home assistant IP": "家庭助理IP",
"Home assistant WS Port": "家庭助理 WS 端口:",
"Password": "密码:",
"Password repeat": "密码重复:",
"Passwords missmatch!": "密码不匹配!",
"Secure": "HTTPS",
"Verbose filter logging": "详细过滤器日志",
"When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "启用后,在适配器初始同步期间单独记录每个被排除的 entity_id(需要日志级别 info 或 debug)。",
"When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "启用此功能后,每次适配器启动时,都会删除 entities.* 下 entity_id 与排除模式匹配的现有对象。具有自定义适配器配置(history/influxdb/sql)的对象将被保留并记录。"
}
-76
View File
@@ -1,76 +0,0 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
function load(settings, onChange) {
if (!settings) return;
if (settings.password === undefined) settings.password = '';
if (settings.prompt === undefined) settings.prompt = 'hass>';
$('.value').each(function () {
var key = $(this).attr('id');
var $value = $('#' + key + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$value.val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
// Signal to admin, that no changes yet
onChange(false);
}
function save(callback) {
var obj = {};
if ($('#password').val() !== $('#passwordRepeat').val()) {
showMessage(_('Passwords missmatch!'), _('Warning'), 'alert');
return;
}
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
callback(obj);
}
</script>
</head>
<body>
<div id="adapter-container">
<table><tr>
<td><img src="hass.png"/></td>
<td><h3 class="translate">FHEM adapter settings</h3></td>
</tr></table>
<table>
<tr><td><label class="translate" for="host">Home assistant IP:</label></td><td class="admin-icon"></td><td><input class="value" id="host" /></td></tr>
<tr><td><label class="translate" for="port">Home assistant WS Port:</label></td><td class="admin-icon"></td><td><input class="value" id="port" size="5" maxlength="5"/></td></tr>
<tr><td><label class="translate" for="password">Password:</label></td><td class="admin-icon"></td><td><input class="value" id="password" type="password"/></td></tr>
<tr><td><label class="translate" for="passwordRepeat">Password repeat:</label></td><td class="admin-icon"></td><td><input type="password" id="passwordRepeat"/></td></tr>
<tr><td><label class="translate" for="secure">Secure:</label></td><td class="admin-icon"></td><td><input class="value" id="secure" type="checkbox"/></td></tr>
</table>
</div>
</body>
</html>
-75
View File
@@ -1,75 +0,0 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
function load(settings, onChange) {
if (!settings) return;
if (settings.password === undefined) settings.password = '';
if (settings.prompt === undefined) settings.prompt = 'hass>';
$('.value').each(function () {
var key = $(this).attr('id');
var $value = $('#' + key + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$value.val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
// Signal to admin, that no changes yet
onChange(false);
}
function save(callback) {
var obj = {};
if ($('#password').val() !== $('#passwordRepeat').val()) {
showMessage(_('Passwords missmatch!'), _('Warning'), 'alert');
return;
}
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
callback(obj);
}
</script>
</head>
<body>
<div id="adapter-container">
<table><tr>
<td><img src="hass.png" alt="logo"/></td>
<td><h3 class="translate">Home assistant adapter settings</h3></td>
</tr></table>
<table>
<tr><td><label class="translate" for="host">Home assistant IP:</label></td><td class="admin-icon"></td><td><input class="value" id="host" /></td></tr>
<tr><td><label class="translate" for="port">Home assistant WS Port:</label></td><td class="admin-icon"></td><td><input class="value" id="port" size="5" maxlength="5"/></td></tr>
<tr><td><label class="translate" for="password">Password:</label></td><td class="admin-icon"></td><td><input class="value" id="password" type="password"/></td></tr>
<tr><td><label class="translate" for="passwordRepeat">Password repeat:</label></td><td class="admin-icon"></td><td><input type="password" id="passwordRepeat"/></td></tr>
<tr><td><label class="translate" for="secure">Secure:</label></td><td class="admin-icon"></td><td><input class="value" id="secure" type="checkbox"/></td></tr>
</table>
</div>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
{
"i18n": true,
"type": "panel",
"items": {
"host": {
"type": "text",
"label": "Home assistant IP",
"sm": 12,
"md": 6,
"lg": 4
},
"port": {
"type": "number",
"label": "Home assistant WS Port",
"min": 1,
"max": 65535,
"sm": 12,
"md": 6,
"lg": 4
},
"_divider1": {
"type": "divider"
},
"password": {
"type": "password",
"label": "Password",
"repeat": true,
"visible": true,
"sm": 12,
"md": 6,
"lg": 4
},
"_divider2": {
"type": "divider"
},
"secure": {
"type": "checkbox",
"label": "Secure",
"sm": 12,
"md": 6,
"lg": 4
},
"_divider3": {
"type": "divider"
},
"excludePatterns": {
"type": "text",
"label": "Exclude patterns (one per line)",
"help": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.",
"minRows": 6,
"sm": 12
},
"verboseFilterLog": {
"type": "checkbox",
"label": "Verbose filter logging",
"help": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).",
"sm": 12
},
"cleanupExcludedOnStart": {
"type": "checkbox",
"label": "Cleanup excluded entities on adapter start",
"help": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.",
"sm": 12
}
}
}
-19
View File
@@ -1,19 +0,0 @@
systemDictionary = {
"FHEM adapter settings":{"de": "FHEM Adapter Einstellungen", "ru": "Настройки драйвера FHEM"},
"FHEM Telnet IP:": {"de": "FHEM Telnet IP:", "ru": "FHEM Telnet IP:"},
"FHEM Telnet Port:": {"en": "FHEM Telnet Port:", "de": "FHEM Telnet Port:", "ru": "FHEM Telnet порт:"},
"Password:": {"en": "Password:", "de": "Kennwort:", "ru": "Пароль:"},
"Password repeat:": {"en": "Password repeat:", "de": "Kennwort-Woederholung:", "ru": "Подтверждение пароля:"},
"Passwords missmatch!": {"en": "Passwords missmatch!", "de": "Kennworte sind unterschiedlich!", "ru": "Пароли не совпадают!"},
"Prompt:": {"en": "Prompt:", "de": "Prompt:", "ru": "Промпт:"},
"tooltip_password": {
"en": "Optional. Only if FHEM requires password.",
"de": "Optional. Nur falls in FHEM konfiguriert",
"ru": "Не обязательно. Только если настроено в FHEM"
},
"tooltip_prompt": {
"en": "You can change standard prompt from 'hass>' to something else.",
"de": "Man kann hier ein eigenes Prompt setzen. Default ist 'hass>'",
"ru": "Можно задать собственный промпт. По умолчанию 'hass>'"
}
};
+34
View File
@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isExcluded = isExcluded;
exports.buildExcludeRegexps = buildExcludeRegexps;
/**
* Returns true if entityId matches any of the supplied glob patterns.
* Glob syntax: `*` is the only wildcard and matches any sequence of characters
* (including dots). All other characters are matched literally (regex
* metacharacters are escaped). Matching is case-sensitive and anchored to
* the full entity_id.
*
* An empty patterns array always returns false.
*/
function isExcluded(entityId, patterns) {
if (!patterns?.length) {
return false;
}
for (const pattern of patterns) {
if (pattern.test(entityId)) {
return true;
}
}
return false;
}
function buildExcludeRegexps(patterns) {
if (!patterns?.length) {
return [];
}
return patterns.map(pattern => {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(`^${escaped}$`);
});
}
//# sourceMappingURL=entityFilter.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"entityFilter.js","sourceRoot":"","sources":["../../src/lib/entityFilter.ts"],"names":[],"mappings":";;AASA,gCAUC;AAED,kDAQC;AA7BD;;;;;;;;GAQG;AACH,SAAgB,UAAU,CAAC,QAAgB,EAAE,QAAkB;IAC3D,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAgB,mBAAmB,CAAC,QAAkB;IAClD,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,CAAC;IACd,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;QAC1B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACnF,OAAO,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACP,CAAC"}
+215
View File
@@ -0,0 +1,215 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_events_1 = require("node:events");
const ws_1 = __importDefault(require("ws"));
/*
const ERRORS: Record<number, string> = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST',
};
*/
class HASS extends node_events_1.EventEmitter {
socket = null;
options;
log;
currentId = 1;
requests = {};
_connected = false;
connectTimeout = null;
closed = false;
constructor(options, log) {
super();
this.options = {
host: options.host || '127.0.0.1',
port: parseInt(String(options.port), 10) || 8123,
password: options.password,
secure: options.secure,
};
this.log = log;
}
subscribeEvents(socket, callback) {
if (socket && typeof socket.send === 'function') {
const id = this.currentId++;
this.requests[id] = { type: 'subscribe_events', ts: Date.now(), cb: callback };
socket.send(JSON.stringify({
id,
type: 'subscribe_events',
}));
}
else {
callback?.('not connected');
}
}
sendCommand(socket, type, callback, extra) {
if (socket && typeof socket.send === 'function') {
const id = this.currentId++;
this.requests[id] = { type, cb: callback, ts: Date.now() };
socket.send(JSON.stringify({
id,
type,
...extra,
}));
}
else {
callback?.('not connected');
}
}
sendAuth(socket, pass) {
if (socket && typeof socket.send === 'function') {
socket.send(JSON.stringify({
type: 'auth',
access_token: pass,
}));
}
}
initSocket(socket) {
socket.on('message', (msg) => {
const msgStr = msg.toString();
this.log.silly(msgStr);
const response = JSON.parse(msgStr);
if (response.type === 'event') {
if (response.event?.data && response.event.event_type === 'system_log_event') {
if (response.event.data.level === 'WARNING') {
this.log.warn(`EVENT: ${response.event.data.message}`);
}
else if (response.event.data.level === 'ERROR') {
this.log.error(`EVENT: ${response.event.data.message}`);
}
else {
this.log.debug(`EVENT: ${response.event.data.message}`);
}
}
else if (response.event?.event_type === 'state_changed') {
this.emit('state_changed', response.event.data.new_state);
}
}
else if (response.type === 'auth_required') {
const password = this.options.password || process.env.SUPERVISOR_TOKEN;
if (!password) {
this.emit('error', 'Password required. Connection closed');
socket.terminate();
}
else {
setTimeout(() => this.sendAuth(socket, password), 50);
}
}
else if (response.type === 'auth_ok') {
setImmediate(() => this.subscribeEvents(socket, err => {
if (!err) {
this._connected = true;
this.emit('connected');
}
}));
}
else if (response.id === undefined) {
this.log.error(`Invalid answer: ${msgStr}`);
}
else {
if (response.type === 'result' && this.requests[response.id]) {
this.log.debug(`got answer for ${this.requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`);
if (typeof this.requests[response.id].cb === 'function') {
this.requests[response.id].cb(!response.success, response.result);
delete this.requests[response.id];
}
}
}
});
socket.on('error', (err) => {
this.socket = null;
if (err?.message?.indexOf('RSV2 and RSV3 must be clear') !== -1) {
// ignore deflate error
}
else {
this.log.error(err.toString());
}
});
socket.on('open', () => {
// connection opened
});
socket.on('close', () => {
this.socket = null;
if (this._connected) {
this._connected = false;
this.emit('disconnected');
}
if (!this.connectTimeout && !this.closed) {
this.connectTimeout = setTimeout(() => {
this.connectTimeout = null;
this.connect();
}, 3000);
}
});
}
isConnected() {
return this._connected;
}
getConfig(callback) {
if (!this._connected) {
callback('not connected');
}
else {
this.sendCommand(this.socket, 'get_config', callback);
}
}
getStates(callback) {
if (!this._connected) {
callback('not connected');
}
else {
this.sendCommand(this.socket, 'get_states', callback);
}
}
getServices(callback) {
if (!this._connected) {
callback('not connected');
}
else {
this.sendCommand(this.socket, 'get_services', callback);
}
}
getPanels(callback) {
if (!this._connected) {
callback('not connected');
}
else {
this.sendCommand(this.socket, 'get_panels', callback);
}
}
callService(service, domain, serviceData, target, callback) {
if (!this._connected) {
callback('not connected');
}
else {
this.sendCommand(this.socket, 'call_service', callback, {
domain: domain || '',
service,
service_data: serviceData,
target,
});
}
}
connect() {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socket = new ws_1.default(`ws${this.options.secure ? 's' : ''}://${this.options.host}:${this.options.port}/${this.options.host === 'supervisor' ? 'core' : 'api'}/websocket`, { perMessageDeflate: false });
this.initSocket(this.socket);
}
close() {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.closed = true;
if (this.socket) {
this.socket.close();
}
}
}
exports.default = HASS;
//# sourceMappingURL=hass.js.map
File diff suppressed because one or more lines are too long
+830
View File
@@ -0,0 +1,830 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const adapter_core_1 = require("@iobroker/adapter-core");
const hass_1 = __importDefault(require("./lib/hass"));
const entityFilter_1 = require("./lib/entityFilter");
const knownAttributes = {
azimuth: { write: false, read: true, unit: '°' },
elevation: { write: false, read: true, unit: '°' },
};
const mapTypes = {
string: 'string',
number: 'number',
object: 'mixed',
boolean: 'boolean',
};
const skipServices = ['persistent_notification'];
function getRoleForState(entity) {
const domain = entity.domain || entity.entity_id.split('.')[0];
const state = entity.state;
switch (domain) {
case 'light':
return 'switch';
case 'switch':
return 'switch';
case 'binary_sensor':
return 'sensor.binary';
case 'sensor':
if (typeof state === 'number' || !isNaN(parseFloat(String(state)))) {
if (entity.attributes?.unit_of_measurement) {
const unit = entity.attributes.unit_of_measurement;
if (unit === '°C' || unit === '°F' || unit === 'K') {
return 'value.temperature';
}
if (unit === '%') {
return 'value.humidity';
}
if (unit === 'hPa' || unit === 'mbar') {
return 'value.pressure';
}
if (unit === 'W' || unit === 'kW') {
return 'value.power';
}
if (unit === 'V') {
return 'value.voltage';
}
if (unit === 'A') {
return 'value.current';
}
if (unit.indexOf('m/s') !== -1 || unit.indexOf('km/h') !== -1) {
return 'value.speed';
}
}
return 'value';
}
return 'text';
case 'climate':
return 'thermostat';
case 'cover':
return 'blind';
case 'lock':
return 'state';
case 'input_boolean':
return 'switch';
case 'input_number':
return 'level';
case 'input_text':
return 'text';
case 'input_select':
return 'text';
case 'media_player':
return 'media.state';
case 'device_tracker':
return 'state';
case 'scene':
return 'button';
case 'script':
return 'button';
case 'automation':
return 'switch';
case 'vacuum':
return 'state';
case 'weather':
return 'weather';
default:
if (state === 'on' || state === 'off') {
return 'switch';
}
if (typeof state === 'number' || !isNaN(parseFloat(String(state)))) {
return 'value';
}
if (typeof state === 'boolean') {
return 'indicator';
}
return 'state';
}
}
function getRoleForAttribute(attr, value, type) {
const attrLower = attr.toLowerCase();
if (attrLower.includes('temperature')) {
return 'value.temperature';
}
if (attrLower.includes('humidity')) {
return 'value.humidity';
}
if (attrLower.includes('pressure')) {
return 'value.pressure';
}
if (attrLower === 'brightness' || attrLower === 'current_position') {
return 'level.dimmer';
}
if (attrLower === 'rgb_color' || attrLower === 'xy_color') {
return 'level.color.rgb';
}
if (attrLower === 'color_temp') {
return 'level.color.temperature';
}
if (attrLower === 'battery_level' || attrLower === 'battery') {
return 'value.battery';
}
if (attrLower === 'locked') {
return 'indicator';
}
if (attrLower === 'volume_level') {
return 'level.volume';
}
if (attrLower === 'position') {
return 'level';
}
if (attrLower === 'speed' || attrLower === 'percentage') {
return 'level';
}
if (attrLower === 'mode' || attrLower === 'preset_mode') {
return 'text';
}
switch (type) {
case 'number':
return 'value';
case 'boolean':
return 'indicator';
case 'string':
return 'text';
case 'object':
case 'mixed':
case 'array':
return 'json';
default:
return 'state';
}
}
class HassAdapter extends adapter_core_1.Adapter {
hassConnected = false;
hass = null;
hassObjects = {};
delayTimeout = null;
syncDebounceTimeout = null;
stopped = false;
excludePatterns = [];
initialSyncCompleted = false;
constructor(options = {}) {
super({
...options,
name: 'hass',
ready: () => this.main(),
unload: callback => this.onUnload(callback),
stateChange: (id, state) => this.onStateChange(id, state),
});
}
debouncedSync(callback) {
if (this.syncDebounceTimeout) {
clearTimeout(this.syncDebounceTimeout);
}
this.syncDebounceTimeout = setTimeout(() => {
this.syncDebounceTimeout = null;
this.hass.getStates((err, states) => {
if (err) {
this.log.error(`Cannot read states during resync: ${err}`);
return;
}
this.hass.getServices(async (err, services) => {
if (err) {
this.log.error(`Cannot read services during resync: ${err}`);
return;
}
await this.parseStates(states, services);
callback?.();
});
});
}, 3000);
}
onStateChange(id, state) {
if (!state || state.ack) {
return;
}
if (!this.hassConnected) {
this.log.warn(`Cannot send command to "${id}", because not connected`);
return;
}
if (!this.hassObjects[id]) {
return;
}
if (!this.hassObjects[id].common.write) {
this.log.warn(`Object ${id} is not writable!`);
return;
}
// Handle boolean state toggle
if (id.endsWith('.state_boolean')) {
const entityId = this.hassObjects[id].native.entity_id;
const domain = entityId
? entityId.split('.')[0]
: this.hassObjects[id].native.domain || this.hassObjects[id].native.type;
const service = state.val ? 'turn_on' : 'turn_off';
this.log.debug(`Processing boolean state change for ${id}`);
this.log.debug(`Domain: ${domain}, Entity: ${this.hassObjects[id].native.entity_id}, Service: ${service}, Value: ${state.val}`);
if (domain) {
const serviceData = { entity_id: this.hassObjects[id].native.entity_id };
this.hass.callService(service, domain, serviceData, {}, err => {
if (err) {
this.log.error(`Cannot control ${id}: ${err}`);
}
else {
this.log.debug(`Successfully sent command to HASS for ${id}`);
}
});
return;
}
this.log.warn(`No domain found for ${id}`);
}
const serviceData = {};
const fields = this.hassObjects[id].native.fields;
const target = {};
let requestFields = {};
if (typeof state.val === 'string') {
state.val = state.val.trim();
if (state.val.startsWith('{') && state.val.endsWith('}')) {
try {
requestFields = JSON.parse(state.val) || {};
}
catch (err) {
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
requestFields = {};
}
}
}
// If a non-JSON value was set, and we only have one relevant field, use this field as value
if (fields && !Object.keys(requestFields).length) {
const fieldList = Object.keys(fields);
if (fieldList.length === 1 && fieldList[0] !== 'entity_id') {
requestFields[fieldList[0]] = state.val;
}
else if (fieldList.length === 2 && fields.entity_id) {
requestFields[fieldList[1 - fieldList.indexOf('entity_id')]] = state.val;
}
}
this.log.debug(`Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`);
if (fields) {
for (const field in fields) {
if (!Object.prototype.hasOwnProperty.call(fields, field)) {
continue;
}
if (field === 'entity_id') {
target.entity_id = this.hassObjects[id].native.entity_id;
}
else if (requestFields[field] !== undefined) {
serviceData[field] = requestFields[field];
}
}
}
const noFields = Object.keys(serviceData).length === 0;
serviceData.entity_id = this.hassObjects[id].native.entity_id;
this.log.debug(`Send to HASS for service ${this.hassObjects[id].native.attr} with ${this.hassObjects[id].native.domain || this.hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`);
this.hass.callService(this.hassObjects[id].native.attr, this.hassObjects[id].native.domain || this.hassObjects[id].native.type, serviceData, target, err => {
if (err) {
this.log.error(`Cannot control ${id}: ${err}`);
}
if (err && fields && noFields) {
this.log.warn(`Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`);
this.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`);
}
});
}
onUnload(callback) {
this.stopped = true;
if (this.delayTimeout) {
clearTimeout(this.delayTimeout);
this.delayTimeout = null;
}
if (this.syncDebounceTimeout) {
clearTimeout(this.syncDebounceTimeout);
this.syncDebounceTimeout = null;
}
this.hass?.close();
callback?.();
}
async syncStates(states) {
if (states?.length) {
for (const state of states) {
const id = state.id;
delete state.id;
try {
await this.setForeignStateAsync(id, state);
}
catch (err) {
this.log.error(err.toString());
}
}
}
}
async syncObjects(objects) {
const stats = { newCount: 0, updatedCount: 0 };
if (objects?.length) {
for (const obj of objects) {
this.hassObjects[obj._id] = obj;
try {
const oldObj = await this.getForeignObjectAsync(obj._id);
if (!oldObj) {
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
this.hassObjects[obj._id] = obj;
await this.setForeignObjectAsync(obj._id, obj);
stats.newCount++;
}
else {
this.hassObjects[obj._id] = oldObj;
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
oldObj.native = obj.native;
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
await this.setForeignObjectAsync(obj._id, oldObj);
stats.updatedCount++;
}
}
}
catch (err) {
this.log.error(err.toString());
}
}
}
return stats;
}
async deleteStaleObjects(expectedObjects) {
const objectsToDelete = [];
let knownCount = 0;
for (const id in this.hassObjects) {
if (Object.prototype.hasOwnProperty.call(this.hassObjects, id) &&
id.startsWith(`${this.namespace}.entities.`)) {
knownCount++;
if (!expectedObjects.has(id)) {
objectsToDelete.push(id);
}
}
}
// Sanity guard: if HASS returned a drastically smaller entity set than what we
// know (e.g. mid-startup after a restart), assume the data is incomplete and
// skip deletion. The next sync will retry.
if (knownCount > 10 && objectsToDelete.length > knownCount / 2) {
this.log.warn(`Skipping deletion of ${objectsToDelete.length}/${knownCount} stale objects — HASS likely returned an incomplete state list. Will retry on next sync.`);
return 0;
}
let deletedCount = 0;
for (const id of objectsToDelete) {
try {
// Never auto-delete objects that hold user configuration like
// common.custom (history/influxdb/sql adapter settings) — losing
// those silently on a transient HASS hiccup would force the user
// to recreate them. See issue #165.
const existing = await this.getForeignObjectAsync(id);
const custom = existing?.common?.custom;
if (custom && Object.keys(custom).length) {
this.log.debug(`Keeping "${id}" despite being stale: object holds custom adapter configuration`);
continue;
}
await this.delObjectAsync(id);
delete this.hassObjects[id];
deletedCount++;
}
catch (err) {
this.log.error(`Error deleting object ${id}: ${err}`);
}
}
return deletedCount;
}
async parseStates(entities, services) {
const objs = [];
const states = [];
const expectedObjects = new Set();
let excludedCount = 0;
const excludedIds = [];
for (let e = 0; e < entities.length; e++) {
const entity = entities[e];
if (!entity) {
continue;
}
if ((0, entityFilter_1.isExcluded)(entity.entity_id, this.excludePatterns)) {
excludedCount++;
if (this.config.verboseFilterLog && !this.initialSyncCompleted) {
excludedIds.push(entity.entity_id);
}
continue;
}
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
const desc = entity.attributes?.attribution || undefined;
const channelId = `${this.namespace}.entities.${entity.entity_id}`;
expectedObjects.add(channelId);
const channel = {
_id: channelId,
common: {
name,
},
type: 'channel',
native: {
object_id: entity.object_id,
entity_id: entity.entity_id,
},
};
if (desc) {
channel.common.desc = desc;
}
objs.push(channel);
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
const stateId = `${channelId}.state`;
expectedObjects.add(stateId);
const obj = {
_id: stateId,
type: 'state',
common: {
name: `${name} STATE`,
type: typeof entity.state,
role: getRoleForState(entity),
read: true,
write: false,
},
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
},
};
if (entity.attributes?.unit_of_measurement) {
obj.common.unit = entity.attributes.unit_of_measurement;
}
objs.push(obj);
let val = entity.state;
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({ id: obj._id, lc, ts, val, ack: true });
// Create boolean state for on/off entities
const boolStateId = `${channelId}.state_boolean`;
expectedObjects.add(boolStateId);
if (!objs.find(o => o._id === boolStateId)) {
const booleanObj = {
_id: boolStateId,
type: 'state',
common: {
name: `${name} STATE_BOOLEAN`,
type: 'boolean',
read: true,
write: true,
role: 'switch',
},
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
attr: 'state',
type: entity.domain,
},
};
objs.push(booleanObj);
states.push({
id: boolStateId,
lc: lc || Date.now(),
ts: ts || Date.now(),
val: entity.state === 'on',
ack: true,
});
}
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
attr === 'friendly_name' ||
attr === 'unit_of_measurement' ||
attr === 'icon' ||
!attr.length) {
continue;
}
let common;
if (knownAttributes[attr]) {
common = { ...knownAttributes[attr] };
}
else {
common = {};
}
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
const fullAttrId = `${channelId}.${attrId}`;
expectedObjects.add(fullAttrId);
const obj = {
_id: fullAttrId,
type: 'state',
common,
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
attr,
},
};
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
common.read ??= true;
common.write ??= false;
common.type ??= mapTypes[typeof entity.attributes[attr]];
common.role ??= getRoleForAttribute(attr, entity.attributes[attr], common.type);
objs.push(obj);
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({ id: obj._id, lc, ts, val, ack: true });
}
}
const serviceType = entity.entity_id.split('.')[0];
if (services[serviceType] && !skipServices.includes(serviceType)) {
const service = services[serviceType];
for (const s in service) {
if (Object.prototype.hasOwnProperty.call(service, s)) {
const serviceId = `${channelId}.${s}`;
expectedObjects.add(serviceId);
const obj = {
_id: serviceId,
type: 'state',
common: {
name: entity.entity_id,
desc: service[s].description,
read: false,
write: true,
type: 'mixed',
role: 'button',
},
native: {
object_id: entity.object_id,
domain: entity.domain,
fields: service[s].fields,
entity_id: entity.entity_id,
attr: s,
type: serviceType,
},
};
objs.push(obj);
}
}
}
}
const deletedCount = await this.deleteStaleObjects(expectedObjects);
const syncStats = await this.syncObjects(objs);
await this.syncStates(states);
if (syncStats.newCount > 0 || deletedCount > 0) {
const changes = [];
if (syncStats.newCount > 0) {
changes.push(`${syncStats.newCount} created`);
}
if (deletedCount > 0) {
changes.push(`${deletedCount} deleted`);
}
this.log.info(`Synchronization completed: ${changes.join(', ')}`);
}
if (excludedCount > 0) {
this.log.info(`Entity filter excluded ${excludedCount} entit${excludedCount === 1 ? 'y' : 'ies'} from sync`);
}
if (excludedIds.length > 0) {
for (const id of excludedIds) {
this.log.info(`Entity filter excluded: ${id}`);
}
}
this.initialSyncCompleted = true;
}
async cleanupExcludedObjects() {
if (!this.config.cleanupExcludedOnStart) {
return;
}
if (this.excludePatterns.length === 0) {
this.log.info('Cleanup skipped: no exclude patterns configured');
return;
}
let allObjects;
try {
allObjects = await this.getAdapterObjectsAsync();
}
catch (err) {
this.log.error(`Cleanup: failed to load adapter objects: ${err}`);
return;
}
const prefix = `${this.namespace}.entities.`;
// Group every object under entities.* by its derived entity_id. We extract
// the entity_id from the object id (first two path components after the
// prefix) instead of native.entity_id — older objects from previous adapter
// versions may have been written as flat states without a parent channel
// and without native.entity_id, but their id still encodes the entity.
const matchedByEntity = new Map();
for (const id in allObjects) {
if (!Object.prototype.hasOwnProperty.call(allObjects, id) || !id.startsWith(prefix)) {
continue;
}
const rest = id.substring(prefix.length);
const parts = rest.split('.');
if (parts.length < 2) {
continue;
}
const entityId = `${parts[0]}.${parts[1]}`;
if (!(0, entityFilter_1.isExcluded)(entityId, this.excludePatterns)) {
continue;
}
const ids = matchedByEntity.get(entityId);
if (ids) {
ids.push(id);
}
else {
matchedByEntity.set(entityId, [id]);
}
}
if (matchedByEntity.size === 0) {
this.log.info('Cleanup: no existing objects matched exclude patterns');
return;
}
let deletedEntityCount = 0;
let deletedIdCount = 0;
let keptForCustomCount = 0;
for (const [entityId, ids] of matchedByEntity) {
// Custom-config protection: scan all ids of the group; if any holds
// common.custom (history/influxdb/sql) keep the whole entity.
let hasCustom = false;
for (const id of ids) {
const custom = allObjects[id].common?.custom;
if (custom && Object.keys(custom).length) {
hasCustom = true;
break;
}
}
if (hasCustom) {
keptForCustomCount++;
this.log.warn(`Cleanup: keeping entity "${entityId}" — has custom adapter config (history/influxdb/sql); remove it manually if you really want to drop it`);
continue;
}
// Delete sub-states first (longest ids), then any parent channel last.
const sortedIds = [...ids].sort((a, b) => b.length - a.length);
let entityFullyDeleted = true;
for (const id of sortedIds) {
try {
await this.delObjectAsync(id);
delete this.hassObjects[id];
deletedIdCount++;
}
catch (err) {
entityFullyDeleted = false;
this.log.error(`Cleanup: failed to delete "${id}": ${err}`);
}
}
if (entityFullyDeleted) {
deletedEntityCount++;
if (this.config.verboseFilterLog) {
this.log.info(`Cleanup: deleted entity "${entityId}" (${ids.length} object${ids.length === 1 ? '' : 's'})`);
}
}
}
this.log.info(`Cleanup: deleted ${deletedEntityCount} excluded entit${deletedEntityCount === 1 ? 'y' : 'ies'} (${deletedIdCount} object${deletedIdCount === 1 ? '' : 's'} total)${keptForCustomCount > 0
? `, kept ${keptForCustomCount} entit${keptForCustomCount === 1 ? 'y' : 'ies'} with custom config (see warnings above)`
: ''}`);
}
async main() {
this.config.host ||= '127.0.0.1';
this.config.port = parseInt(String(this.config.port), 10) || 8123;
const rawPatterns = (this.config.excludePatterns || '').toString();
const stringPatterns = rawPatterns
.split(/\r?\n/)
.map(s => s.trim())
.filter(line => line.length > 0 && !line.startsWith('#'));
this.excludePatterns = (0, entityFilter_1.buildExcludeRegexps)(stringPatterns);
if (this.excludePatterns.length === 0) {
this.log.info('Entity filter inactive (no exclude patterns configured)');
}
else {
this.log.info(`Entity filter active: ${this.excludePatterns.length} pattern(s) loaded: ${stringPatterns.join(', ')}`);
}
await this.cleanupExcludedObjects();
await this.setStateAsync('info.connection', false, true);
this.hass = new hass_1.default(this.config, this.log);
this.hass.on('error', err => this.log.error(err));
this.hass.on('state_changed', async (entity) => {
this.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
if (!entity || typeof entity.entity_id !== 'string') {
return;
}
if ((0, entityFilter_1.isExcluded)(entity.entity_id, this.excludePatterns)) {
this.log.debug(`Entity filter: ignored state_changed for ${entity.entity_id}`);
return;
}
const id = `entities.${entity.entity_id}.`;
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
if (this.hassObjects[`${this.namespace}.${id}state`]) {
await this.setStateAsync(`${id}state`, { val: entity.state, ack: true, lc, ts });
}
else {
this.log.info(`State changed for unknown object ${id}state. Triggering synchronization to resync the objects.`);
this.debouncedSync();
}
// Update boolean state
if (this.hassObjects[`${this.namespace}.${id}state_boolean`]) {
await this.setStateAsync(`${id}state_boolean`, {
val: entity.state === 'on',
ack: true,
lc: lc || Date.now(),
ts: ts || Date.now(),
});
}
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
attr === 'friendly_name' ||
attr === 'unit_of_measurement' ||
attr === 'icon' ||
!attr.length) {
continue;
}
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
if (this.hassObjects[`${this.namespace}.${id}state`]) {
const fullAttrId = `${this.namespace}.${id}${attrId}`;
if (!this.hassObjects[fullAttrId]) {
// Attribute appeared after initial sync — create object dynamically
const common = {
...knownAttributes[attr],
name: attr.replace(/_/g, ' '),
read: true,
write: false,
role: 'state',
type: mapTypes[typeof entity.attributes[attr]] ?? 'mixed',
};
const newObj = {
_id: fullAttrId,
type: 'state',
common,
native: { entity_id: entity.entity_id, attr },
};
this.log.debug(`Creating missing attribute object ${fullAttrId}`);
await this.setForeignObjectAsync(fullAttrId, newObj);
this.hassObjects[fullAttrId] = newObj;
}
await this.setStateAsync(id + attrId, { val, ack: true, lc, ts });
}
else {
this.log.info(`State changed for unknown object ${id + attrId}. Triggering synchronization to resync the objects.`);
this.debouncedSync();
}
}
}
});
this.hass.on('connected', () => {
if (!this.hassConnected) {
this.log.debug('Connected');
this.hassConnected = true;
void this.setState('info.connection', true, true);
this.hass.getConfig(err => {
if (err) {
this.log.error(`Cannot read config: ${err}`);
return;
}
this.delayTimeout = setTimeout(() => {
this.delayTimeout = null;
if (!this.stopped) {
this.hass.getStates((err, states) => {
if (this.stopped) {
return;
}
if (err) {
this.log.error(`Cannot read states: ${err}`);
return;
}
this.delayTimeout = setTimeout(() => {
this.delayTimeout = null;
if (!this.stopped) {
this.hass.getServices(async (err, services) => {
if (this.stopped) {
return;
}
if (err) {
this.log.error(`Cannot read services: ${err}`);
}
else {
await this.parseStates(states, services);
this.log.info('Initialization completed');
await this.subscribeStatesAsync('*');
}
});
}
}, 100);
});
}
}, 100);
});
}
});
this.hass.on('disconnected', () => {
if (this.hassConnected) {
this.log.debug('Disconnected');
this.hassConnected = false;
void this.setState('info.connection', false, true);
}
});
this.hass.connect();
}
}
exports.default = HassAdapter;
if (require.main !== module) {
// Export the constructor in compact mode
module.exports = (options) => new HassAdapter(options);
}
else {
// otherwise start the instance directly
(() => new HassAdapter())();
}
//# sourceMappingURL=main.js.map
File diff suppressed because one or more lines are too long
+35
View File
@@ -0,0 +1,35 @@
import config from '@iobroker/eslint-config';
export default [
...config,
{
languageOptions: {
parserOptions: {
allowDefaultProject: {
allow: ['*.js', '*.mjs'],
},
tsconfigRootDir: import.meta.dirname,
// project: './tsconfig.json',
},
},
},
{
// disable temporary the rule 'jsdoc/require-param' and enable 'jsdoc/require-jsdoc'
rules: {
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param': 'off',
'jsdoc/check-param-names': 'off',
},
},
{
ignores: [
'node_modules/**/*',
'build/**/*',
'admin/**/*',
'test/**/*',
'src-admin/**/*',
'tmp/**/*',
'**/*.mjs',
],
},
];
-403
View File
@@ -1,403 +0,0 @@
'use strict';
const gulp = require('gulp');
const fs = require('fs');
const pkg = require('./package.json');
const iopackage = require('./io-package.json');
const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
/*var appName = getAppName();
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 1].split('.')[0].toLowerCase();
}
*/
const fileName = 'words.js';
let languages = {
en: {},
de: {},
ru: {},
pt: {},
nl: {},
fr: {},
it: {},
es: {},
pl: {},
'zh-cn': {}
};
function lang2data(lang, isFlat) {
let str = isFlat ? '' : '{\n';
let count = 0;
for (const w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
if (isFlat) {
str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n';
} else {
const key = ' "' + w.replace(/"/g, '\\"') + '": ';
str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n';
}
}
}
if (!count) {
return isFlat ? '' : '{\n}';
}
if (isFlat) {
return str;
} else {
return str.substring(0, str.length - 2) + '\n}';
}
}
function readWordJs(src) {
try {
let words;
if (fs.existsSync(src + 'js/' + fileName)) {
words = fs.readFileSync(src + 'js/' + fileName).toString();
} else {
words = fs.readFileSync(src + fileName).toString();
}
const lines = words.split(/\r\n|\r|\n/g);
let i = 0;
while (!lines[i].match(/^systemDictionary = {/)) {
i++;
}
lines.splice(0, i);
// remove last empty lines
i = lines.length - 1;
while (!lines[i]) {
i--;
}
if (i < lines.length - 1) {
lines.splice(i + 1);
}
lines[0] = lines[0].replace('systemDictionary = ', '');
lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}');
words = lines.join('\n');
const resultFunc = new Function('return ' + words + ';');
return resultFunc();
} catch (e) {
return null;
}
}
function padRight(text, totalLength) {
return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : '');
}
function writeWordJs(data, src) {
let text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
text += '/*global systemDictionary:true */\n';
text += '\'use strict\';\n\n';
text += 'systemDictionary = {\n';
for (const word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
let line = '';
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' ';
}
}
if (line) {
line = line.trim();
line = line.substring(0, line.length - 1);
}
text += line + '},\n';
}
}
text = text.replace(/},\n$/, '}\n');
text += '};';
if (fs.existsSync(src + 'js/' + fileName)) {
fs.writeFileSync(src + 'js/' + fileName, text);
} else {
fs.writeFileSync(src + '' + fileName, text);
}
}
const EMPTY = '';
function words2languages(src) {
const langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (const l in langs) {
if (!langs.hasOwnProperty(l)) continue;
const keys = Object.keys(langs[l]);
//keys.sort();
let obj = {};
for (let k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
if (!fs.existsSync(src + 'i18n/' + l)) {
fs.mkdirSync(src + 'i18n/' + l);
}
fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj));
}
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function words2languagesFlat(src) {
let langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
const keys = Object.keys(langs.en);
//keys.sort();
for (const l in langs) {
if (!langs.hasOwnProperty(l)) continue;
let obj = {};
for (let k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
langs[l] = obj;
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (const ll in langs) {
if (!langs.hasOwnProperty(ll)) continue;
if (!fs.existsSync(src + 'i18n/' + ll)) {
fs.mkdirSync(src + 'i18n/' + ll);
}
fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en));
}
fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n'));
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function languagesFlat2words(src) {
const dirs = fs.readdirSync(src + 'i18n/');
let langs = {};
let bigOne = {};
const order = Object.keys(languages);
dirs.sort((a, b) => {
const posA = order.indexOf(a);
const posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
for (let l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
let lang = dirs[l];
const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
langs[lang] = {};
keys.forEach(function (word, i) {
langs[lang][word] = values[i].replace(/<\/ i>/g, '</i>').replace(/<\/ b>/g, '</b>').replace(/<\/ span>/g, '</span>').replace(/% s/g, ' %s');
});
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
const aWords = readWordJs();
const temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt'];
if (aWords) {
// Merge words together
for (const w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w]
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1) return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
function languages2words(src) {
const dirs = fs.readdirSync(src + 'i18n/');
let langs = {};
let bigOne = {};
const order = Object.keys(languages);
dirs.sort((a, b) => {
const posA = order.indexOf(a);
const posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
for (let l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
const lang = dirs[l];
langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString();
langs[lang] = JSON.parse(langs[lang]);
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
const aWords = readWordJs();
const temporaryIgnore = ['pt', 'fr', 'nl', 'it'];
if (aWords) {
// Merge words together
for (const w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w]
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1) return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
gulp.task('admin (words.js => json)', done => {
words2languages('./admin/');
done();
});
gulp.task('admin (words.js => flat)', done => {
words2languagesFlat('./admin/');
done();
});
gulp.task('admin (flat => words.js)', done => {
languagesFlat2words('./admin/');
done();
});
gulp.task('admin (json => words.js)', done => {
languages2words('./admin/');
done();
});
gulp.task('updatePackages', done => {
iopackage.common.version = pkg.version;
iopackage.common.news = iopackage.common.news || {};
if (!iopackage.common.news[pkg.version]) {
const news = iopackage.common.news;
let newNews = {};
newNews[pkg.version] = {
en: 'news',
de: 'neues',
ru: 'новое'
};
iopackage.common.news = Object.assign(newNews, news);
}
fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4));
done();
});
gulp.task('updateReadme', done => {
const readme = fs.readFileSync('README.md').toString();
const pos = readme.indexOf('## Changelog\n');
if (pos !== -1) {
let readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
let readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) === -1) {
const timestamp = new Date();
const date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
let news = '';
if (iopackage.common.news && iopackage.common.news[pkg.version]) {
news += '* ' + iopackage.common.news[pkg.version].en;
}
fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd);
}
}
done();
});
gulp.task('default', gulp.series('updatePackages', 'updateReadme'));
+216 -49
View File
@@ -1,19 +1,19 @@
{
"common": {
"name": "hass",
"version": "1.0.1",
"title": "Home Assistant",
"version": "2.1.0",
"titleLang": {
"en": "Home Assistant",
"de": "Home-Assistent",
"ru": "Домашний помощник",
"de": "Home Assistant",
"ru": "Home Assistant",
"pt": "Home Assistant",
"nl": "Thuisassistent",
"fr": "Assistante à domicile",
"it": "Assistente domiciliare",
"es": "Asistente de hogar",
"pl": "Asystent domowy",
"zh-cn": "家庭助理"
"nl": "Home Assistant",
"fr": "Home Assistant",
"it": "Home Assistant",
"es": "Home Assistant",
"pl": "Home Assistant",
"zh-cn": "Home Assistant",
"uk": "Home Assistant"
},
"desc": {
"en": "Home Assistant connection for ioBroker",
@@ -24,53 +24,119 @@
"fr": "Connexion Home Assistant pour ioBroker",
"it": "Connessione Home Assistant per ioBroker",
"es": "Conexión de Home Assistant para ioBroker",
"pl": "Połączenie Home Assistant dla ioBroker"
"pl": "Połączenie Home Assistant dla ioBroker",
"zh-cn": "ioBroker 的家庭助理连接",
"uk": "Підключення Home Assistant для ioBroker"
},
"news": {
"1.0.1": {
"en": "IMPORTANT: js-controller 2.0 is needed st least!\nFix start issue\n(Apollon77/Garfonso) Fix issue where value could not be set in hass",
"de": "WICHTIG: js-controller 2.0 wird am wenigsten benötigt!\nStartproblem beheben\n(Apollon77/Garfonso) Problem behoben, bei dem der Wert nicht in Hass festgelegt werden konnte",
"ru": "ВАЖНО: js-controller 2.0 нужен как минимум!\nИсправить проблему с запуском\n(Apollon77 / Garfonso) Исправлена ошибка, из-за которой значение не могло быть установлено в hass.",
"pt": "IMPORTANTE: o js-controller 2.0 é o mínimo necessário!\nCorrigir problema inicial\n(Apollon77 / Garfonso) Correção do problema em que o valor não podia ser definido em hass",
"nl": "BELANGRIJK: js-controller 2.0 is minimaal nodig!\nStartprobleem oplossen\n(Apollon77/Garfonso) Probleem opgelost waarbij de waarde niet kon worden ingesteld in hass",
"fr": "IMPORTANT : js-controller 2.0 est au moins nécessaire !\nRésoudre le problème de démarrage\n(Apollon77/Garfonso) Correction d'un problème où la valeur ne pouvait pas être définie dans hass",
"it": "IMPORTANTE: come minimo è necessario js-controller 2.0!\nRisolvi il problema di avvio\n(Apollon77/Garfonso) Risolto il problema per cui il valore non poteva essere impostato in hass",
"es": "IMPORTANTE: ¡js-controller 2.0 es lo menos necesario!\nSolucionar problema de inicio\n(Apollon77 / Garfonso) Se solucionó el problema por el cual no se podía establecer el valor en hass",
"pl": "WAŻNE: js-controller 2.0 jest potrzebny przynajmniej!\nNapraw problem z uruchomieniem\n(Apollon77/Garfonso) Napraw problem, w którym nie można było ustawić wartości w hass",
"zh-cn": "重要提示:至少需要 js-controller 2.0\n修复启动问题\n(Apollon77/Garfonso) 修复无法在 hass 中设置值的问题"
"2.1.0": {
"en": "Added optional entity exclude filter with glob patterns, configurable via the admin UI, plus a verbose-logging toggle for inspecting matches\nUse `/core/` instead of `/api/` when connecting to supervisor directly (e.g., in ha app)\nUse ENV var SUPERVISOR_TOKEN as fallback for password",
"de": "Zusätzliche optionale Entität schließt Filter mit Glocke-Muster aus, konfigurierbar über den Admin UI, plus ein Verbose-Logging-Toggle für die Überprüfung von Übereinstimmungen\nVerwenden Sie `/core/` anstelle von `/api/`, wenn Sie direkt mit dem Supervisor (z.B. in ha App)\nVerwenden Sie ENV var SUPERVISOR TOKEN als Rückfall für das Passwort",
"ru": "Добавленный дополнительный объект исключает фильтр с шаблонами шаров, настраиваемый через пользовательский интерфейс администратора, а также переключатель verbose-logging для проверки совпадений\nИспользуйте «/core/» вместо «/api/» при подключении непосредственно к диспетчеру (например, в приложении ha)\nИспользуйте ENV var SUPERVISOR TOKEN в качестве резервного копия для пароля",
"pt": "Adicionada entidade opcional excluir filtro com padrões glob, configurável através da interface de administração, além de uma opção de registro de verbose para inspecionar correspondências\nUtilizar `/core/` em vez de `/api/` ao ligar directamente ao supervisor (por exemplo, em ha app)\nUse ENV var SUPERVISOR TOKEN como recurso para senha",
"nl": "Toegevoegd optionele entiteit filter met glob patronen uitsluiten, configureerbaar via de admin UI, plus een werkbose-logging toggle voor het inspecteren van lucifers\nGebruik \nGebruik ENV var SUPERVISOR TOKEN als terugval voor wachtwoord",
"fr": "Ajout d'une entité optionnelle excluant le filtre avec des modèles glob, configurable via l'interface utilisateur admin, plus un toggle verbose-logging pour l'inspection des correspondances\nUtiliser `/core/` au lieu de `/api/` pour se connecter directement au superviseur (par exemple, dans l'application ha)\nUtiliser ENV var SUPERVISOR TOKEN comme retour au mot de passe",
"it": "Aggiunta di entità facoltativa escludere il filtro con i modelli glob, configurabile tramite l'interfaccia utente di amministrazione, più un gioco di registrazione verbose per ispezionare le partite\nUtilizzare `/core/` invece di `/api/` quando si collega direttamente al supervisore (ad esempio, in ha app)\nUtilizzare ENV var SUPERVISOR TOKEN come failback per la password",
"es": "La entidad opcional agregada excluye el filtro con patrones de glob, configurable a través de la interfaz de usuario del administrador, además de una mezcla de verbose-logging para inspeccionar los partidos\nUse `/core/` en lugar de `/api/` cuando se conecte al supervisor directamente (por ejemplo, en la aplicación ha)\nUtilizar ENV var SUPERVISOR TOKEN como inconveniente para contraseña",
"pl": "Dodano opcjonalny podmiot wyłączający filtr ze wzorami glob, konfigurowalny za pomocą interfejsu użytkownika admin, plus verbose- logowanie przełączanie do kontroli meczów\nUżyj '/ core /' zamiast '/ api /' podczas bezpośredniego połączenia z przełożonym (np. w aplikacji ha)\nUżyj ENV var SUPERVISOR _ TOKEN jako awaryjnego hasła",
"uk": "Додана додаткова особа виключить фільтр з лобовими візерунками, налаштовується через адміністратор UI, а також словесний блок для перевірок матчів\nВикористовуйте `/core/` замість `/api/` при підключенні до супервайзера безпосередньо (наприклад, у додатку га)\nВикористовуйте ENV var SUPERVISOR TOKEN як випадання для пароля",
"zh-cn": "添加的可选实体排除带有 glob 模式的过滤器, 通过 admin UI 可配置, 外加用于检查匹配的动词日志\n在直接连接主管时使用`/core/'而不是`/api/'(例如在ha app中)\n使用 ENV var SUPERVISOR TOKEN 作为密码的倒置"
},
"1.0.0": {
"en": "added the support of compact mode",
"de": "fügte die Unterstützung des Kompaktmodus hinzu",
"ru": "добавлена поддержка компактного режима",
"pt": "adicionado o suporte do modo compacto",
"nl": "de ondersteuning van de compacte modus toegevoegd",
"fr": "ajouté le support du mode compact",
"it": "aggiunto il supporto della modalità compatta",
"es": "agregó el soporte del modo compacto",
"pl": "dodano obsługę trybu kompaktowego",
"zh-cn": "增加了对紧凑模式的支持"
"2.0.4": {
"en": "Tried to keep the custom settings of the objects when updating them with new data from HASS",
"de": "Versuche, die benutzerdefinierten Einstellungen der Objekte bei der Aktualisierung mit neuen Daten von HASS zu halten",
"ru": "Пытался сохранить пользовательские настройки объектов при обновлении их новыми данными из HASS",
"pt": "Tentou manter as configurações personalizadas dos objetos ao atualizá-los com novos dados do HASS",
"nl": "Geprobeerd om de aangepaste instellingen van de objecten te behouden bij het bijwerken ervan met nieuwe gegevens van HASS",
"fr": "Essai de conserver les paramètres personnalisés des objets lors de leur mise à jour avec de nouvelles données de HASS",
"it": "Ho cercato di mantenere le impostazioni personalizzate degli oggetti durante l'aggiornamento con nuovi dati da HASS",
"es": "Trató de mantener la configuración personalizada de los objetos al actualizarlos con nuevos datos de HASS",
"pl": "Próbował zachować niestandardowe ustawienia obiektów podczas ich aktualizacji nowymi danymi z HASS",
"uk": "При оновленні нових даних від HASS",
"zh-cn": "在用 HASS 的新数据更新对象时尝试保留对象的自定义设置"
},
"0.1.0": {
"en": "initial version",
"de": "erste Version",
"ru": "Первоначальная версия",
"pt": "versão inicial",
"nl": "eerste versie",
"fr": "version initiale",
"it": "versione iniziale",
"es": "versión inicial",
"pl": "początkowa wersja"
"2.0.3": {
"en": "Adapter was updated and migrated to TypeScript\nAdded roles for states and added debouncing for reading states from hass",
"de": "Adapter wurde aktualisiert und auf TypeScript migriert\nHinzugefügt Rollen für Staaten und hinzugefügt Debouncing für Lesezustände aus hass",
"ru": "Адаптер был обновлен и перенесен на TypeScript\nДобавлены роли для штатов и добавлены отговорки для чтения штатов из хэша",
"pt": "O adaptador foi atualizado e migrado para TypeScript\nAdicionado papéis para estados e debuncing adicionado para estados de leitura de hass",
"nl": "Adapter is bijgewerkt en gemigreerd naar TypeScript\nToegevoegde rollen voor staten en toegevoegd debouncing voor het lezen van staten uit hass",
"fr": "Adaptateur a été mis à jour et migré vers TypeScript\nAjout de rôles pour les états et ajout de débonflage pour les états de lecture de hass",
"it": "L'adattatore è stato aggiornato e migrato a TypeScript\nAggiunti i ruoli per gli stati e aggiunto debouncing per la lettura di stati da hass",
"es": "Adaptador fue actualizado y migrado a TipoScript\nFunciones adicionales para los estados y desembolsos añadidos para la lectura de estados de hass",
"pl": "Adapter został zaktualizowany i przeniesiony do TypeScript\nDodano role dla państw i dodano debouncing do czytania stanów z hass",
"uk": "Адаптер був оновлений і мігрований до TypeScript\nДодано ролі для штатів і додано деблінг для читання станів з",
"zh-cn": "适应器被更新并迁移到 TypeScript\n添加状态角色, 添加读状态的解跳"
},
"1.4.0": {
"en": "Added more guidance logging when setting services incorrectly\nPrevent crashes when attributes contain \".\" at the end of their names\nAdded logging for state updates for unknown objects",
"de": "Mehr Anleitungsprotokollierung hinzugefügt, wenn Dienste falsch eingestellt werden\nVerhindern Sie Abstürzen, wenn Attribute \" enthalten.\" am Ende ihrer Namen\nHinzugefügt Protokollierung für Zustandsaktualisierungen für unbekannte Objekte",
"ru": "Добавили больше руководства, когда установка услуг неправильно\nПредотвратить сбои, когда атрибуты содержат \".\" в конце их имен\nДобавлен вход для государственных обновлений для неизвестных объектов",
"pt": "Adicionado mais registro de orientação ao definir serviços incorretamente\nImpedir falhas quando os atributos contêm \". no final de seus nomes\nRegisto adicionado para atualizações de estado para objetos desconhecidos",
"nl": "Voeg meer begeleiding toe als ze onjuiste diensten indienen\nVertaling:\nVermeld inloggen op staatsinformatie voor onbekende objecten",
"fr": "Ajout d'autres lignes directrices lorsqu'on établit des services incorrectement\nPrévenir les crashes lorsque les attributs contiennent \".\" à la fin de leurs noms\nAjout de logging pour les mises à jour de l'état des objets inconnus",
"it": "Aggiunto più guida logging quando imposta i servizi in modo errato\nPrevenire crash quando gli attributi contengono \". alla fine dei loro nomi\nAggiunto logging per aggiornamenti di stato per oggetti sconosciuti",
"es": "Se agregó más registro de guía cuando se establecen los servicios incorrectamente\nEvitar fallos cuando los atributos contienen \".\" al final de sus nombres\nRegistro añadido para actualizaciones estatales para objetos desconocidos",
"pl": "Wprowadzone są bardziej wytyczne, gdy ustawienie usług nieprawidłowo błędnie\nPrevent rozbija się, gdy atrybuty zawierają „”\nWprowadzone logowanie dla aktualizacji stanów nieznanych obiektów",
"uk": "Додайте більше звітів при установці послуг неправильно\nПрофілактика аварійних випадків, коли атрибути містять \".\" в кінці їх імен\nДодана записка для державних оновлень для невідомих об'єктів",
"zh-cn": "在不正确地建立服务时增加指导标志\n在名称末尾含有“第2款”的预防事故\n增加用于不明物品的国家更新的伐木"
},
"1.3.0": {
"en": "Further optimize sending data to HASS and allow to set values like numbers as normal states if the service has one attribute and it can be mapped",
"de": "Weitere optimieren Sie das Senden von Daten an HASS und ermöglichen es, Werte wie Zahlen als normale Zustände festzulegen, wenn der Dienst ein Attribut hat und es abgebildet werden kann",
"ru": "Далее оптимизируйте отправку данных в HASS и позвольте установить значения, такие как цифры, как нормальные состояния, если услуга имеет один атрибут, и она может быть загружена",
"pt": "Além disso otimizar o envio de dados para HASS e permitir definir valores como números como estados normais se o serviço tem um atributo e pode ser mapeado",
"nl": "Verder optimiliseer het sturen van gegevens naar HASS en sta toe om waarden als normale staten te plaatsen als de dienst een Attribuut heeft en het kan worden in kaart gebracht",
"fr": "Optimiser l'envoi de données à HASS et permettre de définir des valeurs comme des nombres comme des états normaux si le service a un attribut et il peut être mappé",
"it": "Ottimizzare ulteriormente l'invio di dati a HASS e consentire di impostare valori come numeri come stati normali se il servizio ha un attributo e può essere mappato",
"es": "Optimizar aún más el envío de datos a HASS y permitir establecer valores como números como estados normales si el servicio tiene un atributo y puede ser mapeado",
"pl": "Dalsze optymalizowanie danych do HASS i umożliwienie ustalenie wartości takich jak stany normalne, jeśli usługa ma jeden atrybut i może być zmapowana",
"zh-cn": "进一步优化向人道协调厅系统传送数据,如果该服务具有属性,它可以绘制图表,从而能够将数字象正常国家一样。",
"uk": "Додатково оптимізуйте надсилання даних до HASS і дозвольте встановлювати такі значення, як числа, як звичайні стани, якщо служба має один атрибут і його можна зіставити"
},
"1.2.0": {
"en": "IMPORTANT: Replace special characters in entity attribute names with an underscore! Object IDs might change!\nmake sure a \"null\" value in state changes is not crashing",
"de": "WICHTIG: Ersetzen Sie Sonderzeichen in Namen des Attributes der Einheit mit einem Unterpunkt! Objekt-IDs können sich ändern!\nsicherstellen, dass ein nullwert bei zustandsänderungen nicht abstürzt",
"ru": "ВАЖНО: Замените специальные символы в имени сущности с подчеркиванием! ID объекта может измениться!\nубедитесь, что значение null в государственных изменениях не падает",
"pt": "IMPORTANTE: Substitua caracteres especiais em nomes de atributos de entidade com um sublinhado! IDs de objeto podem mudar!\ncertifique-se de que um valor nulo em mudanças de estado não está caindo",
"nl": "Vervang speciale personages in entiteitsadvocaten met een onderscore! ID's kunnen veranderen!\nzorg ervoor dat een waarde van de staat niet crasht",
"fr": "IMPORTANT: Remplacer les caractères spéciaux dans les noms d'attributs d'entité par un accent! Les identifiants d'objet peuvent changer !\ns'assurer qu'une valeur nulle dans les changements d'état ne s'écrase pas",
"it": "IMPORTANTE: Sostituisci caratteri speciali in nomi attributo entità con un underscore! Gli ID degli Oggetti potrebbero cambiare!\nassicurarsi che un valore nullo nei cambiamenti di stato non è crash",
"es": "IMPORTANTE: Reemplazar caracteres especiales en nombres de atributos de entidad con un subrayado! ¡Las identificaciones de objetos pueden cambiar!\nasegúrese de que un valor nulo en los cambios estatales no se estrella",
"pl": "IMPORTANT: Replace special characters in identity attribution names with an underscore! (ang.). Zmiany obiektów mogą się zmieniać!\nniewątpliwą wartością w zmianach stanowych nie jest rozbicie się",
"zh-cn": "IMPORTANT:将实体的特色性改为强调的! 目标国际组织可能改变!\n确保在国家变化中确保无效不失",
"uk": "ВАЖЛИВО: Замініть спеціальні символи в іменах атрибутів сутності на підкреслення! Ідентифікатори об'єктів можуть змінитися!\nпереконайтеся, що \"нульове\" значення в змінах стану не аварійно завершує роботу"
},
"1.1.2": {
"en": "Fix crash cases reported by Sentry",
"de": "Von Sentry gemeldete Absturzfälle beheben",
"ru": "Исправить случаи сбоев, о которых сообщил Sentry",
"pt": "Corrigir casos de travamento relatados pelo Sentinela",
"nl": "Herstel crashgevallen gemeld door Sentry",
"fr": "Correction des cas de plantage signalés par Sentry",
"it": "Risolvi i casi di crash segnalati da Sentry",
"es": "Solucionar casos de fallas informados por Sentry",
"pl": "Napraw przypadki awarii zgłoszone przez Sentry",
"zh-cn": "修复 Sentry 报告的崩溃案例",
"uk": "Виправте випадки збоїв, про які повідомляє Sentry"
}
},
"localLink": "http://%host%:8123",
"localLinks": {
"_default": "http://%host%:8123"
},
"platform": "Javascript/Node.js",
"mode": "daemon",
"icon": "hass.png",
"enabled": true,
"compact": true,
"materialize": true,
"license": "MIT",
"nogit": true,
"adminUI": {
"config": "json"
},
"licenseInformation": {
"type": "free",
"license": "MIT"
},
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.hass/master/admin/hass.png",
"keywords": [
"HASS",
@@ -85,19 +151,120 @@
],
"dependencies": [
{
"js-controller": ">=2.0.0"
"js-controller": ">=6.0.11"
}
],
"globalDependencies": [
{
"admin": ">=7.6.17"
}
],
"plugins": {
"sentry": {
"dsn": "https://1b2a1e8335f84cc49a67dd5ac2b07573@sentry.iobroker.net/182"
}
},
"connectionType": "local",
"dataSource": "push",
"tier": 2
"tier": 2,
"messages": [
{
"condition": {
"operand": "and",
"rules": [
"oldVersion<1.1.0",
"newVersion>=1.1.0"
]
},
"title": {
"en": "Re-enter the password after Update!",
"de": "Passwort muss nach dem Update erneut eingegeben werden!",
"ru": "Повторно введите пароль после обновления!",
"pt": "Digite novamente a senha após Atualizar!",
"nl": "Voer het wachtwoord opnieuw in na Update!",
"fr": "Entrez à nouveau le mot de passe après la mise à jour !",
"it": "Reinserire la password dopo l'aggiornamento!",
"es": "Vuelva a ingresar la contraseña después de ¡Actualizar!",
"pl": "Wprowadź hasło ponownie po aktualizacji!",
"zh-cn": "更新后重新输入密码!",
"uk": "Повторно введіть пароль після оновлення!"
},
"text": {
"en": "After installing this update the password/token used to access the HASS installation needs to be re-entered in the Instance-Settings!",
"de": "Nach der Installation dieses Updates muss das Passwort/Token für den Zugriff auf die HASS-Installation in den Instanz-Einstellungen erneut eingegeben werden!",
"ru": "После установки этого обновления пароль/токен, используемый для доступа к установке HASS, необходимо повторно ввести в настройках экземпляра!",
"pt": "Depois de instalar esta atualização, a senha/token usado para acessar a instalação do HASS precisa ser reinserido nas configurações da instância!",
"nl": "Na het installeren van deze update moet het wachtwoord/token dat wordt gebruikt om toegang te krijgen tot de HASS-installatie opnieuw worden ingevoerd in de Instance-Settings!",
"fr": "Après l'installation de cette mise à jour, le mot de passe/jeton utilisé pour accéder à l'installation HASS doit être ressaisi dans les paramètres d'instance !",
"it": "Dopo aver installato questo aggiornamento, la password/token utilizzato per accedere all'installazione HASS deve essere reinserito nelle impostazioni dell'istanza!",
"es": "¡Después de instalar esta actualización, la contraseña/token utilizada para acceder a la instalación de HASS debe volver a ingresarse en la configuración de la instancia!",
"pl": "Po zainstalowaniu tej aktualizacji hasło/token używane do uzyskania dostępu do instalacji HASS należy ponownie wprowadzić w ustawieniach instancji!",
"zh-cn": "安装此更新后,用于访问 HASS 安装的密码/令牌需要在实例设置中重新输入!",
"uk": "Після встановлення цього оновлення пароль/токен, який використовується для доступу до встановлення HASS, потрібно повторно ввести в налаштуваннях екземпляра!"
},
"level": "warn",
"buttons": [
"agree",
"cancel"
]
},
{
"condition": {
"operand": "and",
"rules": [
"oldVersion<1.2.0",
"newVersion>=1.2.0"
]
},
"title": {
"en": "Important notice!",
"de": "Wichtiger Hinweis!",
"ru": "Важное замечание!",
"pt": "Notícia importante!",
"nl": "Belangrijke mededeling!",
"fr": "Avis important!",
"it": "Avviso IMPORTANTE!",
"es": "Noticia importante!",
"pl": "Ważna uwaga!",
"zh-cn": "重要通知!",
"uk": "Важливе повідомлення!"
},
"text": {
"en": "The new version 1.2.x will potentially change the Names of Objects when special characters like \".\", \"-\" or spaces were used in entity attributes! Please delete the old objects manually if needed.",
"de": "Die neue Version 1.2.x wird die Namen von Objekten möglicherweise ändern, wenn spezielle Zeichen wie \".\", \"-\" oder Leerzeichen in Entity-Attributen verwendet wurden! Alten Objekte bitte bei Bedarf manuell löschen.",
"ru": "Новая версия 1.2.x будет потенциально менять имена объектов, когда специальные символы типа \".\", \"-\" или пространства были использованы в атрибутах лица! Пожалуйста, удалите старые объекты вручную, если это необходимо.",
"pt": "A nova versão 1.2.x irá potencialmente alterar os Nomes de Objetos quando caracteres especiais como \".\", \"-\" ou espaços foram usados em atributos de entidade! Por favor, exclua os objetos antigos manualmente, se necessário.",
"nl": "De nieuwe versie 1.2.x zal de Namen van Objects veranderen als speciale personages zoals \", \" of ruimtes werden gebruikt in entiteitsaanklachten! Verwijder de oude objecten handmatig als het nodig is.",
"fr": "La nouvelle version 1.2.x modifiera potentiellement les Noms des Objets lorsque des caractères spéciaux comme \", \"-\" ou des espaces ont été utilisés dans des attributs d'entité ! Veuillez supprimer les anciens objets manuellement si nécessaire.",
"it": "La nuova versione 1.2.x cambierà potenzialmente i nomi degli oggetti quando caratteri speciali come \".\", \"-\" o spazi sono stati utilizzati negli attributi dell'entità! Si prega di eliminare i vecchi oggetti manualmente se necessario.",
"es": "La nueva versión 1.2.x potencialmente cambiará los Nombres de Objetos cuando caracteres especiales como \"., \"-\" o espacios fueron utilizados en atributos de entidad! Por favor, borre los viejos objetos manualmente si es necesario.",
"pl": "Nowa wersja 1.2.x będzie potencjalnie zmieniać nazwy obiektów, kiedy szczególne postacie takie jak „.”, „-” lub przestrzenie były używane w atrybutach. Okazuje się, że stare obiekty ręcznie będą potrzebne.",
"zh-cn": "新版本1.2.x将可能改变物体的名称,如“......”、“-”或空间在实体的属性中使用。 请在必要时删除旧物体。.",
"uk": "Нова версія 1.2.x потенційно змінить назви об’єктів, якщо в атрибутах об’єктів використовувалися спеціальні символи, такі як «.», «-» або пробіли! За потреби видаліть старі об’єкти вручну."
},
"level": "warn",
"buttons": [
"agree",
"cancel"
]
}
]
},
"native": {
"host": "127.0.0.1",
"port": 8123,
"password": "",
"secure": false
"secure": false,
"excludePatterns": "# Glob patterns, one per line. Lines starting with # are comments.\n# Example: filter out all entities prefixed with `iob_` regardless of domain:\n# *.iob_*",
"verboseFilterLog": false,
"cleanupExcludedOnStart": false
},
"protectedNative": [
"password"
],
"encryptedNative": [
"password"
],
"objects": [],
"instanceObjects": [
{
-269
View File
@@ -1,269 +0,0 @@
const util = require('util');
const EventEmitter = require('events').EventEmitter;
const WebSocket = require('ws');
function HASS(options, log) {
if (!(this instanceof HASS)) {
return new HASS(options);
}
options = options || {};
options.host = options.host || '127.0.0.1';
options.port = parseInt(options.port, 10) || 8123;
const ERRORS = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST'
};
this.socket = null;
const that = this;
let currentId = 1;
const requests = {};
let connected;
let connectTimeout = null;
let closed = false;
function subscribeEvents(socket, callback) {
if (socket && typeof socket.send === 'function') {
const id = currentId++;
requests[id] = {type: 'subscribe_events', ts: Date.now(), cb: callback};
socket.send(JSON.stringify({
id: id,
type: 'subscribe_events'
/*event_type: 'state_changed'*/
}));
} else {
callback && callback('not connected');
}
}
function getConfig(socket, callback) {
if (socket && typeof socket.send === 'function') {
const id = currentId++;
requests[id] = {type: 'get_config', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
type: 'get_config'
}));
} else {
callback && callback('not connected');
}
}
function getStates(socket, callback) {
if (socket && typeof socket.send === 'function') {
const id = currentId++;
requests[id] = {type: 'get_states', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
type: 'get_states'
}));
} else {
callback && callback('not connected');
}
}
function getPanels(socket, callback) {
if (socket && typeof socket.send === 'function') {
const id = currentId++;
requests[id] = {type: 'get_panels', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
type: 'get_panels'
}));
} else {
callback && callback('not connected');
}
}
function getServices(socket, callback) {
if (socket && typeof socket.send === 'function') {
const id = currentId++;
requests[id] = {type: 'get_services', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
type: 'get_services'
}));
} else {
callback && callback('not connected');
}
}
function callService(socket, service, domain, serviceData, callback) {
if (socket && typeof socket.send === 'function') {
const id = currentId++;
requests[id] = {type: 'call_service', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
type: 'call_service',
domain: domain || '',
service: service,
service_data: serviceData
}));
} else {
callback && callback('not connected');
}
}
function sendAuth(socket, pass) {
if (socket && typeof socket.send === 'function') {
socket.send(JSON.stringify({
type: 'auth',
access_token: pass
}));
}
}
function initSocket(socket) {
socket.on('message', msg => {
log.silly(msg);
const response = JSON.parse(msg);
if (response.type === 'event') {
if (response.event.data && response.event.event_type === 'system_log_event') {
if (response.event.data.level === 'WARNING') {
log.warn('EVENT: ' + response.event.data.message);
} else
if (response.event.data.level === 'ERROR') {
log.error('EVENT: ' + response.event.data.message);
} else {
log.debug('EVENT: ' + response.event.data.message);
}
} else if (response.event && response.event.event_type === 'state_changed') {
that.emit('state_changed', response.event.data.new_state);
}
} else
if (response.type === 'auth_required') {
if (!options.password) {
that.emit('error', 'Password required. Connection closed');
socket.terminate();
} else {
setTimeout(() => sendAuth(socket, options.password), 50);
}
} else
if (response.type === 'auth_ok') {
setImmediate(() =>
subscribeEvents(socket, err => {
if (!err) {
connected = true;
that.emit('connected');
}
}));
} else if (response.id === undefined) {
log.error(`Invalid answer: ${msg}`);
} else {
if (response.type === 'result' && requests[response.id]) {
log.debug(`got answer for ${requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`);
if (typeof requests[response.id].cb === 'function') {
requests[response.id].cb(!response.success, response.result);
delete requests[response.id];
}
}
}
});
socket.on('error', err => {
socket = null;
if (err && err.message.indexOf('RSV2 and RSV3 must be clear') !== -1) {
// ignore deflate error
} else {
log.error(err);
}
});
socket.on('open', () => {
if (!connected) {
}
});
socket.on('close', () => {
that.socket = null;
if (connected) {
connected = false;
that.emit('disconnected');
}
if (!connectTimeout && !closed) {
setTimeout(() => {
connectTimeout = null;
that.connect();
}, 3000);
}
});
}
this.isConnected = () => connected;
this.getConfig = function (callback) {
if (!connected) {
typeof callback === 'function' && callback('not connected');
} else {
getConfig(this.socket, callback);
}
};
this.getStates = function (callback) {
if (!connected) {
typeof callback === 'function' && callback('not connected');
} else {
getStates(this.socket, callback);
}
};
this.getServices = function (callback) {
if (!connected) {
typeof callback === 'function' && callback('not connected');
} else {
getServices(this.socket, callback);
}
};
this.getPanels = function (callback) {
if (!connected) {
typeof callback === 'function' && callback('not connected');
} else {
getPanels(this.socket, callback);
}
};
this.callService = function (service, domain, serviceData, callback) {
if (!connected) {
typeof callback === 'function' && callback('not connected');
} else {
callService(this.socket, service, domain, serviceData, callback);
}
};
this.connect = function () {
if (connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
this.socket = new WebSocket(`ws${options.secure ? 's' : ''}://${options.host}:${options.port}/api/websocket`, {
perMessageDeflate: false
});
initSocket(this.socket);
};
this.close = function () {
if (connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
closed = true;
if (this.socket) {
this.socket.close();
}
}
return this;
}
// extend the EventEmitter class using our class
util.inherits(HASS, EventEmitter);
module.exports = HASS;
-429
View File
@@ -1,429 +0,0 @@
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
'use strict';
const utils = require('@iobroker/adapter-core');
const HASS = require('./lib/hass');
const adapterName = require('./package.json').name.split('.').pop();
let connected = false;
let hass;
let adapter;
const hassObjects = {};
function startAdapter(options) {
options = options || {};
Object.assign(options, {name: adapterName, unload: stop});
adapter = new utils.Adapter(options);
// is called if a subscribed state changes
adapter.on('stateChange', (id, state) => {
// you can use the ack flag to detect if it is status (true) or command (false)
if (state && !state.ack) {
if (!connected) {
return adapter.log.warn('Cannot send command to "' + id + '", because not connected');
}
/*if (id === adapter.namespace + '.' + '.info.resync') {
queue.push({command: 'resync'});
processQueue();
} else */
if (hassObjects[id]) {
if (!hassObjects[id].common.write) {
adapter.log.warn('Object ' + id + ' is not writable!');
} else {
const serviceData = {};
const fields = hassObjects[id].native.fields;
for (const field in fields) {
if (!fields.hasOwnProperty(field)) {
continue;
}
if (field === 'entity_id') {
serviceData.entity_id = hassObjects[id].native.entity_id
} else {
serviceData[field] = state.val;
}
}
serviceData.entity_id = hassObjects[id].native.entity_id
adapter.log.debug(`Send to HASS for service ${hassObjects[id].native.attr} with ${hassObjects[id].native.domain || hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`)
hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, err =>
err && adapter.log.error('Cannot control ' + id + ': ' + err));
}
}
}
});
// is called when databases are connected and adapter received configuration.
// start here!
adapter.on('ready', main);
return adapter;
}
function stop(callback) {
hass && hass.close();
callback && callback();
}
function getUnit(name) {
name = name.toLowerCase();
if (name.indexOf('temperature') !== -1) {
return '°C';
} else if (name.indexOf('humidity') !== -1) {
return '%';
} else if (name.indexOf('pressure') !== -1) {
return 'hPa';
} else if (name.indexOf('degrees') !== -1) {
return '°';
} else if (name.indexOf('speed') !== -1) {
return 'kmh';
}
return undefined;
}
function syncStates(states, cb) {
if (!states || !states.length) {
return cb();
}
const state = states.shift();
const id = state.id;
delete state.id;
adapter.setForeignState(id, state, err => {
err && adapter.log.error(err);
setImmediate(syncStates, states, cb);
});
}
function syncObjects(objects, cb) {
if (!objects || !objects.length) {
return cb();
}
const obj = objects.shift();
hassObjects[obj._id] = obj;
adapter.getForeignObject(obj._id, (err, oldObj) => {
err && adapter.log.error(err);
if (!oldObj) {
adapter.log.debug('Create "' + obj._id + '": ' + JSON.stringify(obj.common));
hassObjects[obj._id] = obj;
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
setImmediate(syncObjects, objects, cb);
});
} else {
hassObjects[obj._id] = oldObj;
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
oldObj.native = obj.native;
adapter.log.debug('Update "' + obj._id + '": ' + JSON.stringify(obj.common));
adapter.setForeignObject(obj._id, oldObj, err => {
err => adapter.log.error(err);
setImmediate(syncObjects, objects, cb);
});
} else {
setImmediate(syncObjects, objects, cb);
}
}
});
}
function syncRoom(room, members, cb) {
adapter.getForeignObject('enum.rooms.' + room, (err, obj) => {
if (!obj) {
obj = {
_id: 'enum.rooms.' + room,
type: 'enum',
common: {
name: room,
members: members
},
native: {}
};
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
cb();
});
} else {
obj.common = obj.common || {};
obj.common.members = obj.common.members || [];
let changed = false;
for (let m = 0; m < members.length; m++) {
if (obj.common.members.indexOf(members[m]) === -1) {
changed = true;
obj.common.members.push(members[m]);
}
}
if (changed) {
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
cb();
});
} else {
cb();
}
}
});
}
const knownAttributes = {
azimuth: {write: false, read: true, unit: '°'},
elevation: {write: false, read: true, unit: '°'}
};
const ERRORS = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST'
};
const mapTypes = {
'string': 'string',
'number': 'number',
'object': 'mixed',
'boolean': 'boolean'
};
const skipServices = [
'persistent_notification'
];
function parseStates(entities, services, callback) {
const objs = [];
const states = [];
let obj;
let channel;
for (let e = 0; e < entities.length; e++) {
const entity = entities[e];
if (!entity) continue;
const name = entity.name || (entity.attributes && entity.attributes.friendly_name ? entity.attributes.friendly_name : entity.entity_id);
const desc = entity.attributes && entity.attributes.attribution ? entity.attributes.attribution : undefined;
channel = {
_id: `${adapter.namespace}.entities.${entity.entity_id}`,
common: {
name: name
},
type: 'channel',
native: {
object_id: entity.object_id,
entity_id: entity.entity_id
}
};
if (desc) channel.common.desc = desc;
objs.push(channel);
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
obj = {
_id: `${adapter.namespace}.entities.${entity.entity_id}.state`,
type: 'state',
common: {
name: `${name} STATE`,
type: typeof entity.state,
read: true,
write: false
},
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id
}
};
if (entity.attributes && entity.attributes.unit_of_measurement) {
obj.common.unit = entity.attributes.unit_of_measurement;
}
adapter.log.debug(`Found Entity state ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`)
objs.push(obj);
let val = entity.state;
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({id: obj._id, lc, ts, val, ack: true})
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (entity.attributes.hasOwnProperty(attr)) {
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue;
}
let common;
if (knownAttributes[attr]) {
common = Object.assign({}, knownAttributes[attr]);
} else {
common = {};
}
obj = {
_id: `${adapter.namespace}.entities.${entity.entity_id}.${attr}`,
type: 'state',
common: common,
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
attr: attr
}
};
if (!common.name) {
common.name = name + ' ' + attr.replace(/_/g, ' ');
}
if (common.read === undefined) {
common.read = true;
}
if (common.write === undefined) {
common.write = false;
}
if (common.type === undefined) {
common.type = mapTypes[typeof entity.attributes[attr]];
}
adapter.log.debug(`Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`)
objs.push(obj);
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({id: obj._id, lc, ts, val, ack: true});
}
}
}
const serviceType = entity.entity_id.split('.')[0];
if (services[serviceType] && !skipServices.includes(serviceType)) {
const service = services[serviceType];
for (const s in service) {
if (service.hasOwnProperty(s)) {
obj = {
_id: `${adapter.namespace}.entities.${entity.entity_id}.${s}`,
type: 'state',
common: {
desc: service[s].description,
read: false,
write: true
},
native: {
object_id: entity.object_id,
domain: entity.domain,
fields: service[s].fields,
entity_id: entity.entity_id,
attr: s,
type: serviceType
}
};
adapter.log.debug(`Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`)
objs.push(obj);
}
}
}
}
syncObjects(objs, () =>
syncStates(states, callback));
}
function main() {
adapter.config.host = adapter.config.host || '127.0.0.1';
adapter.config.port = parseInt(adapter.config.port, 10) || 8123;
adapter.setState('info.connection', false, true);
hass = new HASS(adapter.config, adapter.log);
hass.on('error', err =>
adapter.log.error(err));
hass.on('state_changed', entity => {
adapter.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
const id = adapter.namespace + '.entities.' + entity.entity_id + '.';
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
adapter.setForeignState(id + 'state', {val: entity.state, ack: true, lc: lc, ts: ts});
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (!entity.attributes.hasOwnProperty(attr) || attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue;
}
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
adapter.setForeignState(id + attr, {val, ack: true, lc, ts});
}
}
});
hass.on('connected', () => {
if (!connected) {
adapter.log.debug('Connected');
connected = true;
adapter.setState('info.connection', true, true);
hass.getConfig((err, config) => {
if (err) {
adapter.log.error('Cannot read config: ' + err);
return;
}
//adapter.log.debug(JSON.stringify(config));
setTimeout(() =>
hass.getStates((err, states) => {
if (err) {
return adapter.log.error('Cannot read states: ' + err);
}
//adapter.log.debug(JSON.stringify(states));
setTimeout(() =>
hass.getServices((err, services) => {
if (err) {
adapter.log.error('Cannot read states: ' + err);
} else {
//adapter.log.debug(JSON.stringify(services));
parseStates(states, services, () => {
adapter.log.debug('Initial parsing of states done, subscribe to ioBroker states');
adapter.subscribeStates('*');
});
}
}), 100);
}), 100);
});
}
});
hass.on('disconnected', () => {
if (connected) {
adapter.log.debug('Disconnected');
connected = false;
adapter.setState('info.connection', false, true);
}
});
hass.connect();
}
// If started as allInOne/compact mode => return function to create instance
if (module && module.parent) {
module.exports = startAdapter;
} else {
// or start the instance directly
startAdapter();
}
+8610
View File
File diff suppressed because it is too large Load Diff
+36 -11
View File
@@ -1,11 +1,14 @@
{
"name": "iobroker.hass",
"version": "1.0.1",
"version": "2.1.0",
"description": "Home Assistant",
"author": {
"name": "bluefox",
"email": "dogafox@gmail.com"
},
"engines": {
"node": ">=20"
},
"contributors": [
{
"name": "bluefox",
@@ -25,20 +28,42 @@
"url": "https://github.com/ioBroker/ioBroker.hass"
},
"dependencies": {
"websocket": "^1.0.34",
"ws": "^8.2.1",
"@iobroker/adapter-core": "^2.5.1"
"ws": "^8.20.1",
"@iobroker/adapter-core": "^3.3.2"
},
"devDependencies": {
"@alcalzone/release-script": "^2.2.1",
"gulp": "^4.0.2",
"mocha": "^9.1.1",
"chai": "^4.3.4"
"@alcalzone/release-script": "^5.2.1",
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
"@alcalzone/release-script-plugin-license": "^5.2.0",
"@iobroker/adapter-dev": "^1.5.0",
"@iobroker/build-tools": "^3.0.1",
"@iobroker/eslint-config": "^2.3.4",
"@iobroker/legacy-testing": "^2.0.2",
"@iobroker/testing": "^5.2.2",
"@types/ws": "^8.18.1",
"typescript": "^6.0.3"
},
"main": "main.js",
"main": "build/main.js",
"files": [
"admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
"admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
"build/",
"io-package.json",
"LICENSE"
],
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit",
"release": "node node_modules/@alcalzone/release-script/bin/release.js"
"test:integration": "mocha --exit",
"test:package": "mocha test/testPackageFiles.js --exit",
"test:unit": "mocha test/testEntityFilter.js --exit",
"test": "npm run test:integration",
"build:tsc": "tsc -p tsconfig.build.json",
"build": "npm run build:tsc",
"lint": "eslint -c eslint.config.mjs",
"release": "release-script",
"release-patch": "release-script patch --yes",
"release-minor": "release-script minor --yes",
"release-major": "release-script major --yes",
"translate": "translate-adapter"
},
"bugs": {
"url": "https://github.com/ioBroker/ioBroker.hass/issues"
+3
View File
@@ -0,0 +1,3 @@
import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs';
export default prettierConfig;
+30
View File
@@ -0,0 +1,30 @@
/**
* Returns true if entityId matches any of the supplied glob patterns.
* Glob syntax: `*` is the only wildcard and matches any sequence of characters
* (including dots). All other characters are matched literally (regex
* metacharacters are escaped). Matching is case-sensitive and anchored to
* the full entity_id.
*
* An empty patterns array always returns false.
*/
export function isExcluded(entityId: string, patterns: RegExp[]): boolean {
if (!patterns?.length) {
return false;
}
for (const pattern of patterns) {
if (pattern.test(entityId)) {
return true;
}
}
return false;
}
export function buildExcludeRegexps(patterns: string[]): RegExp[] {
if (!patterns?.length) {
return [];
}
return patterns.map(pattern => {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(`^${escaped}$`);
});
}
+249
View File
@@ -0,0 +1,249 @@
import { EventEmitter } from 'node:events';
import WebSocket from 'ws';
interface HassOptions {
host: string;
port: number;
password?: string;
secure?: boolean;
}
interface HassRequest {
type: string;
ts: number;
cb?: (err: boolean | string | null, result?: any) => void;
}
/*
const ERRORS: Record<number, string> = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST',
};
*/
export default class HASS extends EventEmitter {
private socket: WebSocket | null = null;
private readonly options: HassOptions;
private readonly log: ioBroker.Logger;
private currentId: number = 1;
private readonly requests: Record<number, HassRequest> = {};
private _connected: boolean = false;
private connectTimeout: ReturnType<typeof setTimeout> | null = null;
private closed: boolean = false;
constructor(options: HassOptions, log: ioBroker.Logger) {
super();
this.options = {
host: options.host || '127.0.0.1',
port: parseInt(String(options.port), 10) || 8123,
password: options.password,
secure: options.secure,
};
this.log = log;
}
private subscribeEvents(socket: WebSocket, callback?: (err: boolean | string | null) => void): void {
if (socket && typeof socket.send === 'function') {
const id = this.currentId++;
this.requests[id] = { type: 'subscribe_events', ts: Date.now(), cb: callback };
socket.send(
JSON.stringify({
id,
type: 'subscribe_events',
}),
);
} else {
callback?.('not connected');
}
}
private sendCommand(
socket: WebSocket | null,
type: string,
callback?: (err: boolean | string | null, result?: any) => void,
extra?: Record<string, any>,
): void {
if (socket && typeof socket.send === 'function') {
const id = this.currentId++;
this.requests[id] = { type, cb: callback, ts: Date.now() };
socket.send(
JSON.stringify({
id,
type,
...extra,
}),
);
} else {
callback?.('not connected');
}
}
private sendAuth(socket: WebSocket, pass: string): void {
if (socket && typeof socket.send === 'function') {
socket.send(
JSON.stringify({
type: 'auth',
access_token: pass,
}),
);
}
}
private initSocket(socket: WebSocket): void {
socket.on('message', (msg: WebSocket.Data): void => {
const msgStr = (msg as Buffer).toString();
this.log.silly(msgStr);
const response = JSON.parse(msgStr);
if (response.type === 'event') {
if (response.event?.data && response.event.event_type === 'system_log_event') {
if (response.event.data.level === 'WARNING') {
this.log.warn(`EVENT: ${response.event.data.message}`);
} else if (response.event.data.level === 'ERROR') {
this.log.error(`EVENT: ${response.event.data.message}`);
} else {
this.log.debug(`EVENT: ${response.event.data.message}`);
}
} else if (response.event?.event_type === 'state_changed') {
this.emit('state_changed', response.event.data.new_state);
}
} else if (response.type === 'auth_required') {
const password = this.options.password || process.env.SUPERVISOR_TOKEN;
if (!password) {
this.emit('error', 'Password required. Connection closed');
socket.terminate();
} else {
setTimeout(() => this.sendAuth(socket, password), 50);
}
} else if (response.type === 'auth_ok') {
setImmediate(() =>
this.subscribeEvents(socket, err => {
if (!err) {
this._connected = true;
this.emit('connected');
}
}),
);
} else if (response.id === undefined) {
this.log.error(`Invalid answer: ${msgStr}`);
} else {
if (response.type === 'result' && this.requests[response.id]) {
this.log.debug(
`got answer for ${this.requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`,
);
if (typeof this.requests[response.id].cb === 'function') {
this.requests[response.id].cb!(!response.success, response.result);
delete this.requests[response.id];
}
}
}
});
socket.on('error', (err: Error) => {
this.socket = null;
if (err?.message?.indexOf('RSV2 and RSV3 must be clear') !== -1) {
// ignore deflate error
} else {
this.log.error(err.toString());
}
});
socket.on('open', () => {
// connection opened
});
socket.on('close', () => {
this.socket = null;
if (this._connected) {
this._connected = false;
this.emit('disconnected');
}
if (!this.connectTimeout && !this.closed) {
this.connectTimeout = setTimeout(() => {
this.connectTimeout = null;
this.connect();
}, 3000);
}
});
}
isConnected(): boolean {
return this._connected;
}
getConfig(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_config', callback);
}
}
getStates(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_states', callback);
}
}
getServices(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_services', callback);
}
}
getPanels(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_panels', callback);
}
}
callService(
service: string,
domain: string,
serviceData: Record<string, any>,
target: Record<string, any>,
callback: (err: boolean | string | null, result?: any) => void,
): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'call_service', callback, {
domain: domain || '',
service,
service_data: serviceData,
target,
});
}
}
connect(): void {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socket = new WebSocket(
`ws${this.options.secure ? 's' : ''}://${this.options.host}:${this.options.port}/${this.options.host === 'supervisor' ? 'core' : 'api'}/websocket`,
{ perMessageDeflate: false },
);
this.initSocket(this.socket);
}
close(): void {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.closed = true;
if (this.socket) {
this.socket.close();
}
}
}
+971
View File
@@ -0,0 +1,971 @@
import { Adapter, type AdapterOptions } from '@iobroker/adapter-core';
import HASS from './lib/hass';
import { isExcluded, buildExcludeRegexps } from './lib/entityFilter';
interface HassAdapterConfig {
host: string;
port: number;
password: string;
secure: boolean;
excludePatterns: RegExp[];
verboseFilterLog: boolean;
cleanupExcludedOnStart: boolean;
}
interface HassEntity {
name: string;
attributes: Record<string, any>;
entity_id: string;
object_id: string;
last_changed?: string;
last_updated?: string;
state: ioBroker.StateValue;
domain: string;
}
interface HassServices {
[domain: string]: {
[serviceName: string]: {
description: string;
fields: Record<string, any>;
};
};
}
const knownAttributes: Record<string, { write: boolean; read: boolean; unit: string }> = {
azimuth: { write: false, read: true, unit: '°' },
elevation: { write: false, read: true, unit: '°' },
};
const mapTypes: Record<string, ioBroker.CommonType> = {
string: 'string',
number: 'number',
object: 'mixed',
boolean: 'boolean',
};
const skipServices: string[] = ['persistent_notification'];
function getRoleForState(entity: HassEntity): string {
const domain = entity.domain || entity.entity_id.split('.')[0];
const state = entity.state;
switch (domain) {
case 'light':
return 'switch';
case 'switch':
return 'switch';
case 'binary_sensor':
return 'sensor.binary';
case 'sensor':
if (typeof state === 'number' || !isNaN(parseFloat(String(state)))) {
if (entity.attributes?.unit_of_measurement) {
const unit = entity.attributes.unit_of_measurement;
if (unit === '°C' || unit === '°F' || unit === 'K') {
return 'value.temperature';
}
if (unit === '%') {
return 'value.humidity';
}
if (unit === 'hPa' || unit === 'mbar') {
return 'value.pressure';
}
if (unit === 'W' || unit === 'kW') {
return 'value.power';
}
if (unit === 'V') {
return 'value.voltage';
}
if (unit === 'A') {
return 'value.current';
}
if (unit.indexOf('m/s') !== -1 || unit.indexOf('km/h') !== -1) {
return 'value.speed';
}
}
return 'value';
}
return 'text';
case 'climate':
return 'thermostat';
case 'cover':
return 'blind';
case 'lock':
return 'state';
case 'input_boolean':
return 'switch';
case 'input_number':
return 'level';
case 'input_text':
return 'text';
case 'input_select':
return 'text';
case 'media_player':
return 'media.state';
case 'device_tracker':
return 'state';
case 'scene':
return 'button';
case 'script':
return 'button';
case 'automation':
return 'switch';
case 'vacuum':
return 'state';
case 'weather':
return 'weather';
default:
if (state === 'on' || state === 'off') {
return 'switch';
}
if (typeof state === 'number' || !isNaN(parseFloat(String(state)))) {
return 'value';
}
if (typeof state === 'boolean') {
return 'indicator';
}
return 'state';
}
}
function getRoleForAttribute(attr: string, value: ioBroker.StateValue, type: ioBroker.CommonType): string {
const attrLower = attr.toLowerCase();
if (attrLower.includes('temperature')) {
return 'value.temperature';
}
if (attrLower.includes('humidity')) {
return 'value.humidity';
}
if (attrLower.includes('pressure')) {
return 'value.pressure';
}
if (attrLower === 'brightness' || attrLower === 'current_position') {
return 'level.dimmer';
}
if (attrLower === 'rgb_color' || attrLower === 'xy_color') {
return 'level.color.rgb';
}
if (attrLower === 'color_temp') {
return 'level.color.temperature';
}
if (attrLower === 'battery_level' || attrLower === 'battery') {
return 'value.battery';
}
if (attrLower === 'locked') {
return 'indicator';
}
if (attrLower === 'volume_level') {
return 'level.volume';
}
if (attrLower === 'position') {
return 'level';
}
if (attrLower === 'speed' || attrLower === 'percentage') {
return 'level';
}
if (attrLower === 'mode' || attrLower === 'preset_mode') {
return 'text';
}
switch (type) {
case 'number':
return 'value';
case 'boolean':
return 'indicator';
case 'string':
return 'text';
case 'object':
case 'mixed':
case 'array':
return 'json';
default:
return 'state';
}
}
class HassAdapter extends Adapter {
declare config: HassAdapterConfig;
private hassConnected: boolean = false;
private hass: HASS | null = null;
private readonly hassObjects: Record<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
private syncDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
private stopped: boolean = false;
private excludePatterns: RegExp[] = [];
private initialSyncCompleted: boolean = false;
public constructor(options: Partial<AdapterOptions> = {}) {
super({
...options,
name: 'hass',
ready: () => this.main(),
unload: callback => this.onUnload(callback),
stateChange: (id, state) => this.onStateChange(id, state),
});
}
private debouncedSync(callback?: () => void): void {
if (this.syncDebounceTimeout) {
clearTimeout(this.syncDebounceTimeout);
}
this.syncDebounceTimeout = setTimeout(() => {
this.syncDebounceTimeout = null;
this.hass!.getStates((err, states) => {
if (err) {
this.log.error(`Cannot read states during resync: ${err}`);
return;
}
this.hass!.getServices(async (err, services) => {
if (err) {
this.log.error(`Cannot read services during resync: ${err}`);
return;
}
await this.parseStates(states, services);
callback?.();
});
});
}, 3000);
}
private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
if (!state || state.ack) {
return;
}
if (!this.hassConnected) {
this.log.warn(`Cannot send command to "${id}", because not connected`);
return;
}
if (!this.hassObjects[id]) {
return;
}
if (!(this.hassObjects[id] as ioBroker.StateObject).common.write) {
this.log.warn(`Object ${id} is not writable!`);
return;
}
// Handle boolean state toggle
if (id.endsWith('.state_boolean')) {
const entityId = this.hassObjects[id].native.entity_id;
const domain = entityId
? entityId.split('.')[0]
: this.hassObjects[id].native.domain || this.hassObjects[id].native.type;
const service = state.val ? 'turn_on' : 'turn_off';
this.log.debug(`Processing boolean state change for ${id}`);
this.log.debug(
`Domain: ${domain}, Entity: ${this.hassObjects[id].native.entity_id}, Service: ${service}, Value: ${state.val}`,
);
if (domain) {
const serviceData = { entity_id: this.hassObjects[id].native.entity_id };
this.hass!.callService(service, domain, serviceData, {}, err => {
if (err) {
this.log.error(`Cannot control ${id}: ${err}`);
} else {
this.log.debug(`Successfully sent command to HASS for ${id}`);
}
});
return;
}
this.log.warn(`No domain found for ${id}`);
}
const serviceData: Record<string, any> = {};
const fields: Record<string, any> = this.hassObjects[id].native.fields;
const target: Record<string, any> = {};
let requestFields: Record<string, any> = {};
if (typeof state.val === 'string') {
state.val = state.val.trim();
if (state.val.startsWith('{') && state.val.endsWith('}')) {
try {
requestFields = JSON.parse(state.val) || {};
} catch (err) {
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${(err as Error).message}`);
requestFields = {};
}
}
}
// If a non-JSON value was set, and we only have one relevant field, use this field as value
if (fields && !Object.keys(requestFields).length) {
const fieldList = Object.keys(fields);
if (fieldList.length === 1 && fieldList[0] !== 'entity_id') {
requestFields[fieldList[0]] = state.val;
} else if (fieldList.length === 2 && fields.entity_id) {
requestFields[fieldList[1 - fieldList.indexOf('entity_id')]] = state.val;
}
}
this.log.debug(
`Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`,
);
if (fields) {
for (const field in fields) {
if (!Object.prototype.hasOwnProperty.call(fields, field)) {
continue;
}
if (field === 'entity_id') {
target.entity_id = this.hassObjects[id].native.entity_id;
} else if (requestFields[field] !== undefined) {
serviceData[field] = requestFields[field];
}
}
}
const noFields = Object.keys(serviceData).length === 0;
serviceData.entity_id = this.hassObjects[id].native.entity_id;
this.log.debug(
`Send to HASS for service ${this.hassObjects[id].native.attr} with ${this.hassObjects[id].native.domain || this.hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`,
);
this.hass!.callService(
this.hassObjects[id].native.attr,
this.hassObjects[id].native.domain || this.hassObjects[id].native.type,
serviceData,
target,
err => {
if (err) {
this.log.error(`Cannot control ${id}: ${err}`);
}
if (err && fields && noFields) {
this.log.warn(
`Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`,
);
this.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`);
}
},
);
}
private onUnload(callback?: () => void): void {
this.stopped = true;
if (this.delayTimeout) {
clearTimeout(this.delayTimeout);
this.delayTimeout = null;
}
if (this.syncDebounceTimeout) {
clearTimeout(this.syncDebounceTimeout);
this.syncDebounceTimeout = null;
}
this.hass?.close();
callback?.();
}
private async syncStates(
states: { id?: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[],
): Promise<void> {
if (states?.length) {
for (const state of states) {
const id = state.id!;
delete state.id;
try {
await this.setForeignStateAsync(id, state);
} catch (err) {
this.log.error((err as Error).toString());
}
}
}
}
private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise<{
newCount: number;
updatedCount: number;
}> {
const stats = { newCount: 0, updatedCount: 0 };
if (objects?.length) {
for (const obj of objects) {
this.hassObjects[obj._id] = obj;
try {
const oldObj = await this.getForeignObjectAsync(obj._id);
if (!oldObj) {
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
this.hassObjects[obj._id] = obj;
await this.setForeignObjectAsync(obj._id, obj);
stats.newCount++;
} else {
this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
oldObj.native = obj.native;
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
await this.setForeignObjectAsync(obj._id, oldObj);
stats.updatedCount++;
}
}
} catch (err) {
this.log.error((err as Error).toString());
}
}
}
return stats;
}
private async deleteStaleObjects(expectedObjects: Set<string>): Promise<number> {
const objectsToDelete: string[] = [];
let knownCount = 0;
for (const id in this.hassObjects) {
if (
Object.prototype.hasOwnProperty.call(this.hassObjects, id) &&
id.startsWith(`${this.namespace}.entities.`)
) {
knownCount++;
if (!expectedObjects.has(id)) {
objectsToDelete.push(id);
}
}
}
// Sanity guard: if HASS returned a drastically smaller entity set than what we
// know (e.g. mid-startup after a restart), assume the data is incomplete and
// skip deletion. The next sync will retry.
if (knownCount > 10 && objectsToDelete.length > knownCount / 2) {
this.log.warn(
`Skipping deletion of ${objectsToDelete.length}/${knownCount} stale objects — HASS likely returned an incomplete state list. Will retry on next sync.`,
);
return 0;
}
let deletedCount = 0;
for (const id of objectsToDelete) {
try {
// Never auto-delete objects that hold user configuration like
// common.custom (history/influxdb/sql adapter settings) — losing
// those silently on a transient HASS hiccup would force the user
// to recreate them. See issue #165.
const existing = await this.getForeignObjectAsync(id);
const custom = (existing?.common as { custom?: Record<string, unknown> } | undefined)?.custom;
if (custom && Object.keys(custom).length) {
this.log.debug(`Keeping "${id}" despite being stale: object holds custom adapter configuration`);
continue;
}
await this.delObjectAsync(id);
delete this.hassObjects[id];
deletedCount++;
} catch (err) {
this.log.error(`Error deleting object ${id}: ${err}`);
}
}
return deletedCount;
}
private async parseStates(entities: HassEntity[], services: HassServices): Promise<void> {
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
const expectedObjects = new Set<string>();
let excludedCount = 0;
const excludedIds: string[] = [];
for (let e = 0; e < entities.length; e++) {
const entity = entities[e];
if (!entity) {
continue;
}
if (isExcluded(entity.entity_id, this.excludePatterns)) {
excludedCount++;
if (this.config.verboseFilterLog && !this.initialSyncCompleted) {
excludedIds.push(entity.entity_id);
}
continue;
}
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
const desc = entity.attributes?.attribution || undefined;
const channelId = `${this.namespace}.entities.${entity.entity_id}`;
expectedObjects.add(channelId);
const channel: ioBroker.ChannelObject = {
_id: channelId,
common: {
name,
},
type: 'channel',
native: {
object_id: entity.object_id,
entity_id: entity.entity_id,
},
};
if (desc) {
channel.common.desc = desc;
}
objs.push(channel);
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
const stateId = `${channelId}.state`;
expectedObjects.add(stateId);
const obj: ioBroker.StateObject = {
_id: stateId,
type: 'state',
common: {
name: `${name} STATE`,
type: typeof entity.state as ioBroker.CommonType,
role: getRoleForState(entity),
read: true,
write: false,
},
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
},
};
if (entity.attributes?.unit_of_measurement) {
obj.common.unit = entity.attributes.unit_of_measurement;
}
objs.push(obj);
let val = entity.state;
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({ id: obj._id, lc, ts, val, ack: true });
// Create boolean state for on/off entities
const boolStateId = `${channelId}.state_boolean`;
expectedObjects.add(boolStateId);
if (!objs.find(o => o._id === boolStateId)) {
const booleanObj: ioBroker.StateObject = {
_id: boolStateId,
type: 'state',
common: {
name: `${name} STATE_BOOLEAN`,
type: 'boolean',
read: true,
write: true,
role: 'switch',
},
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
attr: 'state',
type: entity.domain,
},
};
objs.push(booleanObj);
states.push({
id: boolStateId,
lc: lc || Date.now(),
ts: ts || Date.now(),
val: entity.state === 'on',
ack: true,
});
}
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (
!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
attr === 'friendly_name' ||
attr === 'unit_of_measurement' ||
attr === 'icon' ||
!attr.length
) {
continue;
}
let common: ioBroker.StateCommon;
if (knownAttributes[attr]) {
common = { ...knownAttributes[attr] } as ioBroker.StateCommon;
} else {
common = {} as ioBroker.StateCommon;
}
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
const fullAttrId = `${channelId}.${attrId}`;
expectedObjects.add(fullAttrId);
const obj: ioBroker.StateObject = {
_id: fullAttrId,
type: 'state',
common,
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
attr,
},
};
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
common.read ??= true;
common.write ??= false;
common.type ??= mapTypes[typeof entity.attributes[attr]];
common.role ??= getRoleForAttribute(attr, entity.attributes[attr], common.type);
objs.push(obj);
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({ id: obj._id, lc, ts, val, ack: true });
}
}
const serviceType = entity.entity_id.split('.')[0];
if (services[serviceType] && !skipServices.includes(serviceType)) {
const service = services[serviceType];
for (const s in service) {
if (Object.prototype.hasOwnProperty.call(service, s)) {
const serviceId = `${channelId}.${s}`;
expectedObjects.add(serviceId);
const obj: ioBroker.StateObject = {
_id: serviceId,
type: 'state',
common: {
name: entity.entity_id,
desc: service[s].description,
read: false,
write: true,
type: 'mixed',
role: 'button',
},
native: {
object_id: entity.object_id,
domain: entity.domain,
fields: service[s].fields,
entity_id: entity.entity_id,
attr: s,
type: serviceType,
},
};
objs.push(obj);
}
}
}
}
const deletedCount = await this.deleteStaleObjects(expectedObjects);
const syncStats = await this.syncObjects(objs);
await this.syncStates(states);
if (syncStats.newCount > 0 || deletedCount > 0) {
const changes: string[] = [];
if (syncStats.newCount > 0) {
changes.push(`${syncStats.newCount} created`);
}
if (deletedCount > 0) {
changes.push(`${deletedCount} deleted`);
}
this.log.info(`Synchronization completed: ${changes.join(', ')}`);
}
if (excludedCount > 0) {
this.log.info(
`Entity filter excluded ${excludedCount} entit${excludedCount === 1 ? 'y' : 'ies'} from sync`,
);
}
if (excludedIds.length > 0) {
for (const id of excludedIds) {
this.log.info(`Entity filter excluded: ${id}`);
}
}
this.initialSyncCompleted = true;
}
private async cleanupExcludedObjects(): Promise<void> {
if (!this.config.cleanupExcludedOnStart) {
return;
}
if (this.excludePatterns.length === 0) {
this.log.info('Cleanup skipped: no exclude patterns configured');
return;
}
let allObjects: Record<string, ioBroker.Object>;
try {
allObjects = await this.getAdapterObjectsAsync();
} catch (err) {
this.log.error(`Cleanup: failed to load adapter objects: ${err}`);
return;
}
const prefix = `${this.namespace}.entities.`;
// Group every object under entities.* by its derived entity_id. We extract
// the entity_id from the object id (first two path components after the
// prefix) instead of native.entity_id — older objects from previous adapter
// versions may have been written as flat states without a parent channel
// and without native.entity_id, but their id still encodes the entity.
const matchedByEntity = new Map<string, string[]>();
for (const id in allObjects) {
if (!Object.prototype.hasOwnProperty.call(allObjects, id) || !id.startsWith(prefix)) {
continue;
}
const rest = id.substring(prefix.length);
const parts = rest.split('.');
if (parts.length < 2) {
continue;
}
const entityId = `${parts[0]}.${parts[1]}`;
if (!isExcluded(entityId, this.excludePatterns)) {
continue;
}
const ids = matchedByEntity.get(entityId);
if (ids) {
ids.push(id);
} else {
matchedByEntity.set(entityId, [id]);
}
}
if (matchedByEntity.size === 0) {
this.log.info('Cleanup: no existing objects matched exclude patterns');
return;
}
let deletedEntityCount = 0;
let deletedIdCount = 0;
let keptForCustomCount = 0;
for (const [entityId, ids] of matchedByEntity) {
// Custom-config protection: scan all ids of the group; if any holds
// common.custom (history/influxdb/sql) keep the whole entity.
let hasCustom = false;
for (const id of ids) {
const custom = (allObjects[id].common as { custom?: Record<string, unknown> } | undefined)?.custom;
if (custom && Object.keys(custom).length) {
hasCustom = true;
break;
}
}
if (hasCustom) {
keptForCustomCount++;
this.log.warn(
`Cleanup: keeping entity "${entityId}" — has custom adapter config (history/influxdb/sql); remove it manually if you really want to drop it`,
);
continue;
}
// Delete sub-states first (longest ids), then any parent channel last.
const sortedIds = [...ids].sort((a, b) => b.length - a.length);
let entityFullyDeleted = true;
for (const id of sortedIds) {
try {
await this.delObjectAsync(id);
delete this.hassObjects[id];
deletedIdCount++;
} catch (err) {
entityFullyDeleted = false;
this.log.error(`Cleanup: failed to delete "${id}": ${err}`);
}
}
if (entityFullyDeleted) {
deletedEntityCount++;
if (this.config.verboseFilterLog) {
this.log.info(
`Cleanup: deleted entity "${entityId}" (${ids.length} object${ids.length === 1 ? '' : 's'})`,
);
}
}
}
this.log.info(
`Cleanup: deleted ${deletedEntityCount} excluded entit${deletedEntityCount === 1 ? 'y' : 'ies'} (${deletedIdCount} object${deletedIdCount === 1 ? '' : 's'} total)${
keptForCustomCount > 0
? `, kept ${keptForCustomCount} entit${keptForCustomCount === 1 ? 'y' : 'ies'} with custom config (see warnings above)`
: ''
}`,
);
}
private async main(): Promise<void> {
this.config.host ||= '127.0.0.1';
this.config.port = parseInt(String(this.config.port), 10) || 8123;
const rawPatterns = (this.config.excludePatterns || '').toString();
const stringPatterns = rawPatterns
.split(/\r?\n/)
.map(s => s.trim())
.filter(line => line.length > 0 && !line.startsWith('#'));
this.excludePatterns = buildExcludeRegexps(stringPatterns);
if (this.excludePatterns.length === 0) {
this.log.info('Entity filter inactive (no exclude patterns configured)');
} else {
this.log.info(
`Entity filter active: ${this.excludePatterns.length} pattern(s) loaded: ${stringPatterns.join(', ')}`,
);
}
await this.cleanupExcludedObjects();
await this.setStateAsync('info.connection', false, true);
this.hass = new HASS(this.config, this.log);
this.hass.on('error', err => this.log.error(err));
this.hass.on('state_changed', async entity => {
this.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
if (!entity || typeof entity.entity_id !== 'string') {
return;
}
if (isExcluded(entity.entity_id, this.excludePatterns)) {
this.log.debug(`Entity filter: ignored state_changed for ${entity.entity_id}`);
return;
}
const id = `entities.${entity.entity_id}.`;
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
if (this.hassObjects[`${this.namespace}.${id}state`]) {
await this.setStateAsync(`${id}state`, { val: entity.state, ack: true, lc, ts });
} else {
this.log.info(
`State changed for unknown object ${id}state. Triggering synchronization to resync the objects.`,
);
this.debouncedSync();
}
// Update boolean state
if (this.hassObjects[`${this.namespace}.${id}state_boolean`]) {
await this.setStateAsync(`${id}state_boolean`, {
val: entity.state === 'on',
ack: true,
lc: lc || Date.now(),
ts: ts || Date.now(),
});
}
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (
!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
attr === 'friendly_name' ||
attr === 'unit_of_measurement' ||
attr === 'icon' ||
!attr.length
) {
continue;
}
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
if (this.hassObjects[`${this.namespace}.${id}state`]) {
const fullAttrId = `${this.namespace}.${id}${attrId}`;
if (!this.hassObjects[fullAttrId]) {
// Attribute appeared after initial sync — create object dynamically
const common: ioBroker.StateCommon = {
...(knownAttributes[attr] as ioBroker.StateCommon | undefined),
name: attr.replace(/_/g, ' '),
read: true,
write: false,
role: 'state',
type: mapTypes[typeof entity.attributes[attr]] ?? 'mixed',
};
const newObj: ioBroker.StateObject = {
_id: fullAttrId,
type: 'state',
common,
native: { entity_id: entity.entity_id, attr },
};
this.log.debug(`Creating missing attribute object ${fullAttrId}`);
await this.setForeignObjectAsync(fullAttrId, newObj);
this.hassObjects[fullAttrId] = newObj;
}
await this.setStateAsync(id + attrId, { val, ack: true, lc, ts });
} else {
this.log.info(
`State changed for unknown object ${id + attrId}. Triggering synchronization to resync the objects.`,
);
this.debouncedSync();
}
}
}
});
this.hass.on('connected', () => {
if (!this.hassConnected) {
this.log.debug('Connected');
this.hassConnected = true;
void this.setState('info.connection', true, true);
this.hass!.getConfig(err => {
if (err) {
this.log.error(`Cannot read config: ${err}`);
return;
}
this.delayTimeout = setTimeout(() => {
this.delayTimeout = null;
if (!this.stopped) {
this.hass!.getStates((err, states) => {
if (this.stopped) {
return;
}
if (err) {
this.log.error(`Cannot read states: ${err}`);
return;
}
this.delayTimeout = setTimeout(() => {
this.delayTimeout = null;
if (!this.stopped) {
this.hass!.getServices(async (err, services): Promise<void> => {
if (this.stopped) {
return;
}
if (err) {
this.log.error(`Cannot read services: ${err}`);
} else {
await this.parseStates(states, services);
this.log.info('Initialization completed');
await this.subscribeStatesAsync('*');
}
});
}
}, 100);
});
}
}, 100);
});
}
});
this.hass.on('disconnected', () => {
if (this.hassConnected) {
this.log.debug('Disconnected');
this.hassConnected = false;
void this.setState('info.connection', false, true);
}
});
this.hass.connect();
}
}
export default HassAdapter;
if (require.main !== module) {
// Export the constructor in compact mode
module.exports = (options: Partial<AdapterOptions> | undefined) => new HassAdapter(options);
} else {
// otherwise start the instance directly
(() => new HassAdapter())();
}
-729
View File
@@ -1,729 +0,0 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
// check if tmp directory exists
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
var rootDir = path.normalize(__dirname + '/../../');
var pkg = require(rootDir + 'package.json');
var debug = typeof v8debug === 'object';
pkg.main = pkg.main || 'main.js';
var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/');
adapterName = adapterName[adapterName.length - 2];
var adapterStarted = false;
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 3].split('.')[0];
}
var appName = getAppName().toLowerCase();
var objects;
var states;
var pid = null;
function copyFileSync(source, target) {
var targetFile = target;
//if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join(target, path.basename(source));
}
}
try {
fs.writeFileSync(targetFile, fs.readFileSync(source));
}
catch (err) {
console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)");
}
}
function copyFolderRecursiveSync(source, target, ignore) {
var files = [];
var base = path.basename(source);
if (base === adapterName) {
base = pkg.name;
}
//check if folder needs to be created or integrated
var targetFolder = path.join(target, base);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
//copy
if (fs.lstatSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(function (file) {
if (ignore && ignore.indexOf(file) !== -1) {
return;
}
var curSource = path.join(source, file);
var curTarget = path.join(targetFolder, file);
if (fs.lstatSync(curSource).isDirectory()) {
// ignore grunt files
if (file.indexOf('grunt') !== -1) return;
if (file === 'chai') return;
if (file === 'mocha') return;
copyFolderRecursiveSync(curSource, targetFolder, ignore);
} else {
copyFileSync(curSource, curTarget);
}
});
}
}
if (!fs.existsSync(rootDir + 'tmp')) {
fs.mkdirSync(rootDir + 'tmp');
}
function storeOriginalFiles() {
console.log('Store original files...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) {
objects['system.adapter.admin.0'].common.enabled = false;
}
if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) {
objects['system.adapter.admin.1'].common.enabled = false;
}
fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects));
try {
f = fs.readFileSync(dataDir + 'states.json');
fs.writeFileSync(dataDir + 'states.json.original', f);
}
catch (err) {
console.log('no states.json found - ignore');
}
}
function restoreOriginalFiles() {
console.log('restoreOriginalFiles...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json.original');
fs.writeFileSync(dataDir + 'objects.json', f);
try {
f = fs.readFileSync(dataDir + 'states.json.original');
fs.writeFileSync(dataDir + 'states.json', f);
}
catch (err) {
console.log('no states.json.original found - ignore');
}
}
function checkIsAdapterInstalled(cb, counter, customName) {
customName = customName || pkg.name.split('.').pop();
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsAdapterInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.' + customName + '.0']) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} catch (err) {
}
if (counter > 20) {
console.error('checkIsAdapterInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsAdapterInstalled: wait...');
setTimeout(function() {
checkIsAdapterInstalled(cb, counter + 1);
}, 1000);
}
}
function checkIsControllerInstalled(cb, counter) {
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsControllerInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.certificates']) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} catch (err) {
}
if (counter > 20) {
console.log('checkIsControllerInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsControllerInstalled: wait...');
setTimeout(function() {
checkIsControllerInstalled(cb, counter + 1);
}, 1000);
}
}
function installAdapter(customName, cb) {
if (typeof customName === 'function') {
cb = customName;
customName = null;
}
customName = customName || pkg.name.split('.').pop();
console.log('Install adapter...');
var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js';
// make first install
if (debug) {
child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
} else {
// add controller
var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
waitForEnd(_pid, function () {
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
});
}
}
function waitForEnd(_pid, cb) {
if (!_pid) {
cb(-1, -1);
return;
}
_pid.on('exit', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
_pid.on('close', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
}
function installJsController(cb) {
console.log('installJsController...');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') ||
!fs.existsSync(rootDir + 'tmp/' + appName + '-data')) {
// try to detect appName.js-controller in node_modules/appName.js-controller
// travis CI installs js-controller into node_modules
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"');
// copy all
// stop controller
console.log('Stop controller if running...');
var _pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js stop', {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// copy all files into
if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp');
if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){
console.log('Copy js-controller...');
copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/');
}
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js setup first --console', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
__pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(__pid, function () {
checkIsControllerInstalled(function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
console.log('Setup finished.');
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
});
} else {
// check if port 9000 is free, else admin adapter will be added to running instance
var client = new require('net').Socket();
client.on('error', () => {});
client.connect(9000, '127.0.0.1', function() {
console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.');
process.exit(0);
});
setTimeout(function () {
client.destroy();
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => install from git');
child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', {
cwd: rootDir + 'tmp/',
stdio: [0, 1, 2]
});
} else {
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
child_process.exec('node ' + appName + '.js setup first', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
child_process.fork(appName + '.js', ['setup', 'first'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
}
// let npm install admin and run setup
checkIsControllerInstalled(function () {
var _pid;
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
}, 1000);
}
} else {
setTimeout(function () {
console.log('installJsController: js-controller installed');
if (cb) cb(false);
}, 0);
}
}
function copyAdapterToController() {
console.log('Copy adapter...');
// Copy adapter to tmp/node_modules/appName.adapter
copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']);
console.log('Adapter copied.');
}
function clearControllerLog() {
var dirPath = rootDir + 'tmp/log';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear controller log...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Controller log cleared');
} catch (err) {
console.error('cannot clear log: ' + err);
}
}
}
function clearDB() {
var dirPath = rootDir + 'tmp/iobroker-data/sqlite';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear sqlite DB...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Clear sqlite DB');
} catch (err) {
console.error('cannot clear DB: ' + err);
}
}
}
function setupController(cb) {
installJsController(function (isInited) {
clearControllerLog();
clearDB();
if (!isInited) {
restoreOriginalFiles();
copyAdapterToController();
}
// read system.config object
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var objs;
try {
objs = fs.readFileSync(dataDir + 'objects.json');
objs = JSON.parse(objs);
}
catch (e) {
console.log('ERROR reading/parsing system configuration. Ignore');
objs = {'system.config': {}};
}
if (!objs || !objs['system.config']) {
objs = {'system.config': {}};
}
if (cb) cb(objs['system.config']);
});
}
function startAdapter(objects, states, callback) {
if (adapterStarted) {
console.log('Adapter already started ...');
if (callback) callback(objects, states);
return;
}
adapterStarted = true;
console.log('startAdapter...');
if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) {
try {
if (debug) {
// start controller
pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
} else {
// start controller
pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
}
} catch (error) {
console.error(JSON.stringify(error));
}
} else {
console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main);
}
if (callback) callback(objects, states);
}
function startController(isStartAdapter, onObjectChange, onStateChange, callback) {
if (typeof isStartAdapter === 'function') {
callback = onStateChange;
onStateChange = onObjectChange;
onObjectChange = isStartAdapter;
isStartAdapter = true;
}
if (onStateChange === undefined) {
callback = onObjectChange;
onObjectChange = undefined;
}
if (pid) {
console.error('Controller is already started!');
} else {
console.log('startController...');
adapterStarted = false;
var isObjectConnected;
var isStatesConnected;
var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer');
objects = new Objects({
connection: {
"type" : "file",
"host" : "127.0.0.1",
"port" : 19001,
"user" : "",
"pass" : "",
"noFileCache": false,
"connectTimeout": 2000
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.warn(msg);
},
error: function (msg) {
console.error(msg);
}
},
connected: function () {
isObjectConnected = true;
if (isStatesConnected) {
console.log('startController: started!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onObjectChange
});
// Just open in memory DB itself
var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer');
states = new States({
connection: {
type: 'file',
host: '127.0.0.1',
port: 19000,
options: {
auth_pass: null,
retry_max_delay: 15000
}
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.log(msg);
},
error: function (msg) {
console.log(msg);
}
},
connected: function () {
isStatesConnected = true;
if (isObjectConnected) {
console.log('startController: started!!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onStateChange
});
}
}
function stopAdapter(cb) {
if (!pid) {
console.error('Controller is not running!');
if (cb) {
setTimeout(function () {
cb(false);
}, 0);
}
} else {
adapterStarted = false;
pid.on('exit', function (code, signal) {
if (pid) {
console.log('child process terminated due to receipt of signal ' + signal);
if (cb) cb();
pid = null;
}
});
pid.on('close', function (code, signal) {
if (pid) {
if (cb) cb();
pid = null;
}
});
pid.kill('SIGTERM');
}
}
function _stopController() {
if (objects) {
objects.destroy();
objects = null;
}
if (states) {
states.destroy();
states = null;
}
}
function stopController(cb) {
var timeout;
if (objects) {
console.log('Set system.adapter.' + pkg.name + '.0');
objects.setObject('system.adapter.' + pkg.name + '.0', {
common:{
enabled: false
}
});
}
stopAdapter(function () {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
_stopController();
if (cb) {
cb(true);
cb = null;
}
});
timeout = setTimeout(function () {
timeout = null;
console.log('child process NOT terminated');
_stopController();
if (cb) {
cb(false);
cb = null;
}
pid = null;
}, 5000);
}
// Setup the adapter
function setAdapterConfig(common, native, instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
if (common) objects[id].common = common;
if (native) objects[id].native = native;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects));
}
// Read config of the adapter
function getAdapterConfig(instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
return objects[id];
}
if (typeof module !== undefined && module.parent) {
module.exports.getAdapterConfig = getAdapterConfig;
module.exports.setAdapterConfig = setAdapterConfig;
module.exports.startController = startController;
module.exports.stopController = stopController;
module.exports.setupController = setupController;
module.exports.stopAdapter = stopAdapter;
module.exports.startAdapter = startAdapter;
module.exports.installAdapter = installAdapter;
module.exports.appName = appName;
module.exports.adapterName = adapterName;
module.exports.adapterStarted = adapterStarted;
}
+3
View File
@@ -0,0 +1,3 @@
process.on('unhandledRejection', r => {
throw r;
});
+25 -87
View File
@@ -1,107 +1,45 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
const setup = require('@iobroker/legacy-testing');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
let objects = null;
let states = null;
const onStateChanged = null;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1);
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = function (id, state) {
if (id === 'messagebox.system.adapter.test.0') {
callback(state.message);
}
};
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: (new Date()).getTime()
}
});
}
describe('Test ' + adapterShortName + ' adapter', function() {
before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) {
describe(`Test ${adapterShortName} adapter`, function () {
before(`Test ${adapterShortName} adapter: Start js-controller`, function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(function () {
var config = setup.getAdapterConfig();
setup.setupController(async () => {
const config = await setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
//config.native.dbtype = 'sqlite';
setup.setAdapterConfig(config.common, config.native);
await setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function(id, obj) {}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
setup.startController(
true,
(id, obj) => {},
(id, state) => onStateChanged?.(id, state),
(_objects, _states) => {
objects = _objects;
states = _states;
_done();
});
},
);
});
});
/*
/*
ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY
*/
/*
/*
it('Test ' + adapterShortName + ' adapter: Check if connected', function (done) {
this.timeout(60000);
setTimeout(function () {
@@ -115,11 +53,11 @@ describe('Test ' + adapterShortName + ' adapter', function() {
}, 5000);
});
*/
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) {
after(`Test ${adapterShortName} adapter: Stop js-controller`, function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
setup.stopController(normalTerminated => {
console.log(`Adapter normal terminated: ${normalTerminated}`);
done();
});
});
+48
View File
@@ -0,0 +1,48 @@
const { expect } = require('chai');
const { isExcluded, buildExcludeRegexps } = require('../build/lib/entityFilter');
describe('entityFilter.isExcluded', () => {
it('returns false for an empty pattern list', () => {
expect(isExcluded('switch.iob_anything', [])).to.equal(false);
});
it('matches a simple glob with leading wildcard', () => {
expect(isExcluded('switch.iob_shelly_xyz', buildExcludeRegexps(['*.iob_*']))).to.equal(true);
});
it('matches the bridge mirror naming pattern', () => {
expect(isExcluded('light.iob_ha_eg_wz1__e_licht_decke', buildExcludeRegexps(['*.iob_*__*']))).to.equal(true);
});
it('handles entities that miss a required literal segment', () => {
// *.iob_*__* requires the literal `__` segment — entities without it do not match
const regexps = buildExcludeRegexps(['*.iob_*__*']);
expect(isExcluded('switch.iob_', regexps)).to.equal(false);
expect(isExcluded('switch.iob_foo', regexps)).to.equal(false);
// Naming-Convention safety: similarly named entities without `__` are safe
expect(isExcluded('sensor.scheune_temperatur', buildExcludeRegexps(['*.sc_*__*']))).to.equal(false);
});
it('matches multiple patterns (OR semantic)', () => {
const patterns = buildExcludeRegexps(['*.iob_*__*', '*.knx_*', '*.ha_*__*']);
expect(isExcluded('switch.knx_foo', patterns)).to.equal(true);
expect(isExcluded('switch.ha_eg_wz1__e_licht_decke', patterns)).to.equal(true);
expect(isExcluded('switch.something_else', patterns)).to.equal(false);
});
it('is case sensitive', () => {
expect(isExcluded('switch.IOB_foo', buildExcludeRegexps(['*.iob_*']))).to.equal(false);
expect(isExcluded('switch.iob_foo', buildExcludeRegexps(['*.IOB_*']))).to.equal(false);
});
it('treats * as matching any characters including dots', () => {
// We use full entity_id including the leading domain, so this is fine.
expect(isExcluded('switch.iob_foo', buildExcludeRegexps(['*foo*']))).to.equal(true);
});
it('escapes regex metacharacters in patterns', () => {
// `.` in pattern matches literal `.`, not "any char"
expect(isExcluded('switch.iob_foo', buildExcludeRegexps(['switch.iob_foo']))).to.equal(true);
expect(isExcluded('switchXiob_foo', buildExcludeRegexps(['switch.iob_foo']))).to.equal(false);
});
});
+4 -46
View File
@@ -1,47 +1,5 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
const path = require('path');
const { tests } = require('@iobroker/testing');
describe('Test package.json and io-package.json', function() {
it('Test package files', function (done) {
var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json');
var ioPackage = JSON.parse(fileContentIOPackage);
var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json');
var npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version).to.exist;
expect(npmPackage.version).to.exist;
if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) {
console.log('ERROR: Version numbers in package.json and io-package.json differ!!');
}
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
}
expect(ioPackage.common.authors).to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length).to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0]).to.not.be.equal('my Name <my@email.com>');
}
}
else {
expect(ioPackage.common.authors).to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('Testing for set authors field in io-package skipped because template adapter');
}
done();
});
});
// Validate the package files
tests.packageFiles(path.join(__dirname, '..'));
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"noEmit": false,
"declaration": false,
"rootDir": "src",
"types": ["@iobroker/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"compileOnSave": true,
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"noEmitOnError": true,
"outDir": "./build",
"removeComments": false,
"module": "Node16",
"moduleResolution": "node16",
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true,
"target": "es2022",
"sourceMap": true,
"inlineSourceMap": false,
"useUnknownInCatchVariables": false,
"types": ["@iobroker/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}