Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cdfbd9e4 | ||
|
|
e92d488529 | ||
|
|
906e6d172c | ||
|
|
2f63e3a6dd | ||
|
|
bc74169c7e |
@@ -47,6 +47,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
|
build: true
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: [check-and-lint, adapter-tests]
|
needs: [check-and-lint, adapter-tests]
|
||||||
@@ -68,3 +69,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '22.x'
|
node-version: '22.x'
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
build: true
|
||||||
|
|||||||
@@ -106,8 +106,9 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
### 2.0.1 (2026-03-31)
|
### 2.0.3 (2026-04-02)
|
||||||
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
|
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
|
||||||
|
* (@Titanium177) Added roles for states and added debouncing for reading states from hass
|
||||||
|
|
||||||
### 1.4.0 (2023-01-03)
|
### 1.4.0 (2023-01-03)
|
||||||
* (Apollon77) Added more guidance logging when setting services incorrectly
|
* (Apollon77) Added more guidance logging when setting services incorrectly
|
||||||
|
|||||||
+13
-13
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"name": "hass",
|
"name": "hass",
|
||||||
"version": "2.0.1",
|
"version": "2.0.3",
|
||||||
"title": "Home Assistant",
|
"title": "Home Assistant",
|
||||||
"titleLang": {
|
"titleLang": {
|
||||||
"en": "Home Assistant",
|
"en": "Home Assistant",
|
||||||
@@ -30,18 +30,18 @@
|
|||||||
"uk": "Підключення Home Assistant для ioBroker"
|
"uk": "Підключення Home Assistant для ioBroker"
|
||||||
},
|
},
|
||||||
"news": {
|
"news": {
|
||||||
"2.0.1": {
|
"2.0.3": {
|
||||||
"en": "Adapter was updated and migrated to TypeScript",
|
"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",
|
"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",
|
"ru": "Адаптер был обновлен и перенесен на TypeScript\nДобавлены роли для штатов и добавлены отговорки для чтения штатов из хэша",
|
||||||
"pt": "O adaptador foi atualizado e migrado para TypeScript",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"pl": "Adapter został zaktualizowany i przeniesiony do TypeScript\nDodano role dla państw i dodano debouncing do czytania stanów z hass",
|
||||||
"uk": "Адаптер був оновлений і мігрований до TypeScript",
|
"uk": "Адаптер був оновлений і мігрований до TypeScript\nДодано ролі для штатів і додано деблінг для читання станів з",
|
||||||
"zh-cn": "适应器被更新并迁移到 TypeScript"
|
"zh-cn": "适应器被更新并迁移到 TypeScript\n添加状态角色, 添加读状态的解跳"
|
||||||
},
|
},
|
||||||
"1.4.0": {
|
"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",
|
"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",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "iobroker.hass",
|
"name": "iobroker.hass",
|
||||||
"version": "2.0.1",
|
"version": "2.0.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "iobroker.hass",
|
"name": "iobroker.hass",
|
||||||
"version": "2.0.1",
|
"version": "2.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iobroker/adapter-core": "^3.3.2",
|
"@iobroker/adapter-core": "^3.3.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "iobroker.hass",
|
"name": "iobroker.hass",
|
||||||
"version": "2.0.1",
|
"version": "2.0.3",
|
||||||
"description": "Home Assistant",
|
"description": "Home Assistant",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "bluefox",
|
"name": "bluefox",
|
||||||
|
|||||||
+339
-54
@@ -8,6 +8,26 @@ interface HassAdapterConfig {
|
|||||||
secure: boolean;
|
secure: 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 }> = {
|
const knownAttributes: Record<string, { write: boolean; read: boolean; unit: string }> = {
|
||||||
azimuth: { write: false, read: true, unit: '°' },
|
azimuth: { write: false, read: true, unit: '°' },
|
||||||
elevation: { write: false, read: true, unit: '°' },
|
elevation: { write: false, read: true, unit: '°' },
|
||||||
@@ -22,6 +42,143 @@ const mapTypes: Record<string, ioBroker.CommonType> = {
|
|||||||
|
|
||||||
const skipServices: string[] = ['persistent_notification'];
|
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 {
|
class HassAdapter extends Adapter {
|
||||||
declare config: HassAdapterConfig;
|
declare config: HassAdapterConfig;
|
||||||
|
|
||||||
@@ -29,6 +186,7 @@ class HassAdapter extends Adapter {
|
|||||||
private hass: HASS | null = null;
|
private hass: HASS | null = null;
|
||||||
private readonly hassObjects: Record<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
|
private readonly hassObjects: Record<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
|
||||||
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
|
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private syncDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
private stopped: boolean = false;
|
private stopped: boolean = false;
|
||||||
|
|
||||||
public constructor(options: Partial<AdapterOptions> = {}) {
|
public constructor(options: Partial<AdapterOptions> = {}) {
|
||||||
@@ -41,6 +199,29 @@ class HassAdapter extends Adapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
|
||||||
if (!state || state.ack) {
|
if (!state || state.ack) {
|
||||||
return;
|
return;
|
||||||
@@ -59,8 +240,35 @@ class HassAdapter extends Adapter {
|
|||||||
return;
|
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 serviceData: Record<string, any> = {};
|
||||||
const fields: { entity_id: string } = this.hassObjects[id].native.fields;
|
const fields: Record<string, any> = this.hassObjects[id].native.fields;
|
||||||
const target: Record<string, any> = {};
|
const target: Record<string, any> = {};
|
||||||
|
|
||||||
let requestFields: Record<string, any> = {};
|
let requestFields: Record<string, any> = {};
|
||||||
@@ -70,7 +278,7 @@ class HassAdapter extends Adapter {
|
|||||||
try {
|
try {
|
||||||
requestFields = JSON.parse(state.val) || {};
|
requestFields = JSON.parse(state.val) || {};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
|
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${(err as Error).message}`);
|
||||||
requestFields = {};
|
requestFields = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +343,10 @@ class HassAdapter extends Adapter {
|
|||||||
clearTimeout(this.delayTimeout);
|
clearTimeout(this.delayTimeout);
|
||||||
this.delayTimeout = null;
|
this.delayTimeout = null;
|
||||||
}
|
}
|
||||||
|
if (this.syncDebounceTimeout) {
|
||||||
|
clearTimeout(this.syncDebounceTimeout);
|
||||||
|
this.syncDebounceTimeout = null;
|
||||||
|
}
|
||||||
this.hass?.close();
|
this.hass?.close();
|
||||||
callback?.();
|
callback?.();
|
||||||
}
|
}
|
||||||
@@ -150,13 +362,17 @@ class HassAdapter extends Adapter {
|
|||||||
try {
|
try {
|
||||||
await this.setForeignStateAsync(id, state);
|
await this.setForeignStateAsync(id, state);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err.toString());
|
this.log.error((err as Error).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise<void> {
|
private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise<{
|
||||||
|
newCount: number;
|
||||||
|
updatedCount: number;
|
||||||
|
}> {
|
||||||
|
const stats = { newCount: 0, updatedCount: 0 };
|
||||||
if (objects?.length) {
|
if (objects?.length) {
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
this.hassObjects[obj._id] = obj;
|
this.hassObjects[obj._id] = obj;
|
||||||
@@ -167,36 +383,52 @@ class HassAdapter extends Adapter {
|
|||||||
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
|
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
|
||||||
this.hassObjects[obj._id] = obj;
|
this.hassObjects[obj._id] = obj;
|
||||||
await this.setForeignObjectAsync(obj._id, obj);
|
await this.setForeignObjectAsync(obj._id, obj);
|
||||||
|
stats.newCount++;
|
||||||
} else {
|
} else {
|
||||||
this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
|
this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
|
||||||
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
|
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
|
||||||
oldObj.native = obj.native;
|
oldObj.native = obj.native;
|
||||||
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
|
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
|
||||||
await this.setForeignObjectAsync(obj._id, oldObj);
|
await this.setForeignObjectAsync(obj._id, oldObj);
|
||||||
|
stats.updatedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err.toString());
|
this.log.error((err as Error).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteStaleObjects(expectedObjects: Set<string>): Promise<number> {
|
||||||
|
const objectsToDelete: string[] = [];
|
||||||
|
for (const id in this.hassObjects) {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(this.hassObjects, id) &&
|
||||||
|
id.startsWith(`${this.namespace}.entities.`) &&
|
||||||
|
!expectedObjects.has(id)
|
||||||
|
) {
|
||||||
|
objectsToDelete.push(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseStates(
|
for (const id of objectsToDelete) {
|
||||||
entities: {
|
try {
|
||||||
name: string;
|
await this.delObjectAsync(id);
|
||||||
attributes: { friendly_name?: string; attribution?: string; unit_of_measurement?: string };
|
delete this.hassObjects[id];
|
||||||
entity_id: string;
|
} catch (err) {
|
||||||
object_id: string;
|
this.log.error(`Error deleting object ${id}: ${err}`);
|
||||||
last_changed?: string;
|
}
|
||||||
last_updated?: string;
|
}
|
||||||
state: ioBroker.StateValue;
|
|
||||||
domain: string;
|
return objectsToDelete.length;
|
||||||
}[],
|
}
|
||||||
services: Record<string, { [serviceType: string]: { description: string; fields: { entity_id: string } } }>,
|
|
||||||
): Promise<void> {
|
private async parseStates(entities: HassEntity[], services: HassServices): Promise<void> {
|
||||||
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
|
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
|
||||||
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
|
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
|
||||||
|
const expectedObjects = new Set<string>();
|
||||||
|
|
||||||
for (let e = 0; e < entities.length; e++) {
|
for (let e = 0; e < entities.length; e++) {
|
||||||
const entity = entities[e];
|
const entity = entities[e];
|
||||||
@@ -207,8 +439,11 @@ class HassAdapter extends Adapter {
|
|||||||
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
|
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
|
||||||
const desc = entity.attributes?.attribution || undefined;
|
const desc = entity.attributes?.attribution || undefined;
|
||||||
|
|
||||||
|
const channelId = `${this.namespace}.entities.${entity.entity_id}`;
|
||||||
|
expectedObjects.add(channelId);
|
||||||
|
|
||||||
const channel: ioBroker.ChannelObject = {
|
const channel: ioBroker.ChannelObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}`,
|
_id: channelId,
|
||||||
common: {
|
common: {
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
@@ -227,13 +462,16 @@ class HassAdapter extends Adapter {
|
|||||||
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
|
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
|
||||||
|
|
||||||
if (entity.state !== undefined) {
|
if (entity.state !== undefined) {
|
||||||
|
const stateId = `${channelId}.state`;
|
||||||
|
expectedObjects.add(stateId);
|
||||||
|
|
||||||
const obj: ioBroker.StateObject = {
|
const obj: ioBroker.StateObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}.state`,
|
_id: stateId,
|
||||||
type: 'state',
|
type: 'state',
|
||||||
common: {
|
common: {
|
||||||
name: `${name} STATE`,
|
name: `${name} STATE`,
|
||||||
type: typeof entity.state as ioBroker.CommonType,
|
type: typeof entity.state as ioBroker.CommonType,
|
||||||
role: 'state',
|
role: getRoleForState(entity),
|
||||||
read: true,
|
read: true,
|
||||||
write: false,
|
write: false,
|
||||||
},
|
},
|
||||||
@@ -246,23 +484,57 @@ class HassAdapter extends Adapter {
|
|||||||
if (entity.attributes?.unit_of_measurement) {
|
if (entity.attributes?.unit_of_measurement) {
|
||||||
obj.common.unit = entity.attributes.unit_of_measurement;
|
obj.common.unit = entity.attributes.unit_of_measurement;
|
||||||
}
|
}
|
||||||
this.log.debug(
|
|
||||||
`Found Entity state ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
|
|
||||||
);
|
|
||||||
objs.push(obj);
|
objs.push(obj);
|
||||||
|
|
||||||
let val = entity.state;
|
let val = entity.state;
|
||||||
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
||||||
val = JSON.stringify(val);
|
val = JSON.stringify(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
states.push({ id: obj._id, lc, ts, val, ack: true });
|
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) {
|
if (entity.attributes) {
|
||||||
for (const attr in entity.attributes) {
|
for (const attr in entity.attributes) {
|
||||||
if (Object.prototype.hasOwnProperty.call(entity.attributes, attr)) {
|
if (
|
||||||
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
|
!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
|
||||||
|
attr === 'friendly_name' ||
|
||||||
|
attr === 'unit_of_measurement' ||
|
||||||
|
attr === 'icon' ||
|
||||||
|
!attr.length
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,8 +546,11 @@ class HassAdapter extends Adapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
|
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
|
||||||
|
const fullAttrId = `${channelId}.${attrId}`;
|
||||||
|
expectedObjects.add(fullAttrId);
|
||||||
|
|
||||||
const obj: ioBroker.StateObject = {
|
const obj: ioBroker.StateObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}.${attrId}`,
|
_id: fullAttrId,
|
||||||
type: 'state',
|
type: 'state',
|
||||||
common,
|
common,
|
||||||
native: {
|
native: {
|
||||||
@@ -288,21 +563,12 @@ class HassAdapter extends Adapter {
|
|||||||
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
|
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
|
||||||
common.read ??= true;
|
common.read ??= true;
|
||||||
common.write ??= false;
|
common.write ??= false;
|
||||||
common.type ??=
|
common.type ??= mapTypes[typeof entity.attributes[attr]];
|
||||||
mapTypes[
|
common.role ??= getRoleForAttribute(attr, entity.attributes[attr], common.type);
|
||||||
typeof entity.attributes[
|
|
||||||
attr as 'friendly_name' | 'attribution' | 'unit_of_measurement'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
this.log.debug(
|
|
||||||
`Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
objs.push(obj);
|
objs.push(obj);
|
||||||
|
|
||||||
let val: string | null =
|
let val = entity.attributes[attr];
|
||||||
entity.attributes[attr as 'friendly_name' | 'attribution' | 'unit_of_measurement'] ?? null;
|
|
||||||
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
||||||
val = JSON.stringify(val);
|
val = JSON.stringify(val);
|
||||||
}
|
}
|
||||||
@@ -310,7 +576,6 @@ class HassAdapter extends Adapter {
|
|||||||
states.push({ id: obj._id, lc, ts, val, ack: true });
|
states.push({ id: obj._id, lc, ts, val, ack: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const serviceType = entity.entity_id.split('.')[0];
|
const serviceType = entity.entity_id.split('.')[0];
|
||||||
|
|
||||||
@@ -318,8 +583,11 @@ class HassAdapter extends Adapter {
|
|||||||
const service = services[serviceType];
|
const service = services[serviceType];
|
||||||
for (const s in service) {
|
for (const s in service) {
|
||||||
if (Object.prototype.hasOwnProperty.call(service, s)) {
|
if (Object.prototype.hasOwnProperty.call(service, s)) {
|
||||||
|
const serviceId = `${channelId}.${s}`;
|
||||||
|
expectedObjects.add(serviceId);
|
||||||
|
|
||||||
const obj: ioBroker.StateObject = {
|
const obj: ioBroker.StateObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}.${s}`,
|
_id: serviceId,
|
||||||
type: 'state',
|
type: 'state',
|
||||||
common: {
|
common: {
|
||||||
name: entity.entity_id,
|
name: entity.entity_id,
|
||||||
@@ -327,7 +595,7 @@ class HassAdapter extends Adapter {
|
|||||||
read: false,
|
read: false,
|
||||||
write: true,
|
write: true,
|
||||||
type: 'mixed' as ioBroker.CommonType,
|
type: 'mixed' as ioBroker.CommonType,
|
||||||
role: 'state',
|
role: 'button',
|
||||||
},
|
},
|
||||||
native: {
|
native: {
|
||||||
object_id: entity.object_id,
|
object_id: entity.object_id,
|
||||||
@@ -339,18 +607,26 @@ class HassAdapter extends Adapter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.log.debug(
|
|
||||||
`Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
objs.push(obj);
|
objs.push(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.syncObjects(objs);
|
const deletedCount = await this.deleteStaleObjects(expectedObjects);
|
||||||
|
const syncStats = await this.syncObjects(objs);
|
||||||
await this.syncStates(states);
|
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(', ')}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async main(): Promise<void> {
|
private async main(): Promise<void> {
|
||||||
@@ -378,8 +654,18 @@ class HassAdapter extends Adapter {
|
|||||||
this.setState(`${id}state`, { val: entity.state, ack: true, lc, ts });
|
this.setState(`${id}state`, { val: entity.state, ack: true, lc, ts });
|
||||||
} else {
|
} else {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
`State changed for unknown object ${id}state. Please restart the adapter to resync the objects.`,
|
`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`]) {
|
||||||
|
this.setState(`${id}state_boolean`, {
|
||||||
|
val: entity.state === 'on',
|
||||||
|
ack: true,
|
||||||
|
lc: lc || Date.now(),
|
||||||
|
ts: ts || Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +689,9 @@ class HassAdapter extends Adapter {
|
|||||||
this.setState(id + attrId, { val, ack: true, lc, ts });
|
this.setState(id + attrId, { val, ack: true, lc, ts });
|
||||||
} else {
|
} else {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
`State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`,
|
`State changed for unknown object ${id + attrId}. Triggering synchronization to resync the objects.`,
|
||||||
);
|
);
|
||||||
|
this.debouncedSync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,12 +726,10 @@ class HassAdapter extends Adapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.error(`Cannot read states: ${err}`);
|
this.log.error(`Cannot read services: ${err}`);
|
||||||
} else {
|
} else {
|
||||||
await this.parseStates(states, services);
|
await this.parseStates(states, services);
|
||||||
this.log.debug(
|
this.log.info('Initialization completed');
|
||||||
'Initial parsing of states done, subscribe to ioBroker states',
|
|
||||||
);
|
|
||||||
await this.subscribeStatesAsync('*');
|
await this.subscribeStatesAsync('*');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user