iobroker.loxone 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -183
- package/README.md +417 -411
- package/admin/i18n/de.json +15 -0
- package/admin/i18n/en.json +15 -0
- package/admin/i18n/es.json +18 -0
- package/admin/i18n/fr.json +18 -0
- package/admin/i18n/it.json +18 -0
- package/admin/i18n/nl.json +18 -0
- package/admin/i18n/pl.json +18 -0
- package/admin/i18n/pt.json +18 -0
- package/admin/i18n/ru.json +18 -0
- package/admin/i18n/uk.json +18 -0
- package/admin/i18n/zh-cn.json +18 -0
- package/admin/jsonConfig.json +96 -0
- package/io-package.json +49 -38
- package/package.json +36 -49
- package/src/controls/AalEmergency.ts +90 -0
- package/src/controls/AalSmartAlarm.ts +99 -0
- package/src/controls/Alarm.ts +146 -0
- package/src/controls/AlarmClock.ts +137 -0
- package/src/controls/Application.ts +13 -0
- package/src/controls/AudioZone.ts +227 -0
- package/src/controls/AudioZoneV2.ts +135 -0
- package/src/controls/CentralAlarm.ts +59 -0
- package/src/controls/CentralAudioZone.ts +41 -0
- package/src/controls/CentralGate.ts +53 -0
- package/src/controls/CentralJalousie.ts +67 -0
- package/src/controls/CentralLightController.ts +45 -0
- package/src/controls/ColorPickerV2.ts +34 -0
- package/src/controls/Colorpicker.ts +30 -0
- package/src/controls/ColorpickerBase.ts +326 -0
- package/src/controls/Daytimer.ts +201 -0
- package/src/controls/Dimmer.ts +64 -0
- package/src/controls/EIBDimmer.ts +61 -0
- package/src/controls/Fronius.ts +217 -0
- package/src/controls/Gate.ts +150 -0
- package/src/controls/Hourcounter.ts +115 -0
- package/src/controls/IRCDaytimer.ts +4 -0
- package/src/controls/IRCV2Daytimer.ts +4 -0
- package/src/controls/IRoomControllerV2.ts +595 -0
- package/src/controls/InfoOnlyAnalog.ts +56 -0
- package/src/controls/InfoOnlyDigital.ts +95 -0
- package/src/controls/InfoOnlyText.ts +52 -0
- package/{build/controls/Intercom.js → src/controls/Intercom.ts} +27 -11
- package/src/controls/Jalousie.ts +219 -0
- package/src/controls/LightController.ts +112 -0
- package/src/controls/LightControllerV2.ts +246 -0
- package/src/controls/MailBox.ts +92 -0
- package/src/controls/Meter.ts +94 -0
- package/src/controls/None.ts +29 -0
- package/src/controls/PresenceDetector.ts +47 -0
- package/src/controls/Pushbutton.ts +55 -0
- package/src/controls/Radio.ts +61 -0
- package/{build/controls/Remote.js → src/controls/Remote.ts} +19 -11
- package/src/controls/Slider.ts +78 -0
- package/src/controls/SmokeAlarm.ts +163 -0
- package/src/controls/Switch.ts +46 -0
- package/src/controls/SystemScheme.ts +13 -0
- package/src/controls/TextInput.ts +96 -0
- package/src/controls/TextState.ts +37 -0
- package/src/controls/TimedSwitch.ts +75 -0
- package/src/controls/Tracker.ts +30 -0
- package/{build/controls/Unknown.js → src/controls/Unknown.ts} +18 -11
- package/src/controls/UpDownAnalog.ts +78 -0
- package/{build/controls/ValueSelector.js → src/controls/ValueSelector.ts} +21 -9
- package/src/controls/WindowMonitor.ts +139 -0
- package/src/controls/all-controls.ts +99 -0
- package/src/controls/control-base.ts +28 -0
- package/src/lib/adapter-config.d.ts +22 -0
- package/src/loxone-handler-base.ts +285 -0
- package/src/lxcommunicator.d.ts +1 -0
- package/src/main.test.ts +24 -0
- package/{build/main.js → src/main.ts} +545 -270
- package/src/structure-file.d.ts +154 -0
- package/src/weather-server-handler.ts +266 -0
- package/admin/admin.d.ts +0 -58
- package/admin/index_m.html +0 -126
- package/admin/style.css +0 -32
- package/admin/words.js +0 -25
- package/build/controls/AalEmergency.js +0 -45
- package/build/controls/AalEmergency.js.map +0 -1
- package/build/controls/AalSmartAlarm.js +0 -48
- package/build/controls/AalSmartAlarm.js.map +0 -1
- package/build/controls/Alarm.js +0 -81
- package/build/controls/Alarm.js.map +0 -1
- package/build/controls/AlarmClock.js +0 -59
- package/build/controls/AlarmClock.js.map +0 -1
- package/build/controls/Application.js +0 -11
- package/build/controls/Application.js.map +0 -1
- package/build/controls/AudioZone.js +0 -116
- package/build/controls/AudioZone.js.map +0 -1
- package/build/controls/CentralAlarm.js +0 -30
- package/build/controls/CentralAlarm.js.map +0 -1
- package/build/controls/CentralAudioZone.js +0 -22
- package/build/controls/CentralAudioZone.js.map +0 -1
- package/build/controls/CentralGate.js +0 -30
- package/build/controls/CentralGate.js.map +0 -1
- package/build/controls/CentralJalousie.js +0 -39
- package/build/controls/CentralJalousie.js.map +0 -1
- package/build/controls/CentralLightController.js +0 -27
- package/build/controls/CentralLightController.js.map +0 -1
- package/build/controls/ColorPickerV2.js +0 -24
- package/build/controls/ColorPickerV2.js.map +0 -1
- package/build/controls/Colorpicker.js +0 -20
- package/build/controls/Colorpicker.js.map +0 -1
- package/build/controls/ColorpickerBase.js +0 -263
- package/build/controls/ColorpickerBase.js.map +0 -1
- package/build/controls/Daytimer.js +0 -119
- package/build/controls/Daytimer.js.map +0 -1
- package/build/controls/Dimmer.js +0 -34
- package/build/controls/Dimmer.js.map +0 -1
- package/build/controls/EIBDimmer.js +0 -31
- package/build/controls/EIBDimmer.js.map +0 -1
- package/build/controls/Fronius.js +0 -64
- package/build/controls/Fronius.js.map +0 -1
- package/build/controls/Gate.js +0 -104
- package/build/controls/Gate.js.map +0 -1
- package/build/controls/Hourcounter.js +0 -46
- package/build/controls/Hourcounter.js.map +0 -1
- package/build/controls/IRCDaytimer.js +0 -9
- package/build/controls/IRCDaytimer.js.map +0 -1
- package/build/controls/IRCV2Daytimer.js +0 -9
- package/build/controls/IRCV2Daytimer.js.map +0 -1
- package/build/controls/IRoomControllerV2.js +0 -371
- package/build/controls/IRoomControllerV2.js.map +0 -1
- package/build/controls/InfoOnlyAnalog.js +0 -38
- package/build/controls/InfoOnlyAnalog.js.map +0 -1
- package/build/controls/InfoOnlyDigital.js +0 -65
- package/build/controls/InfoOnlyDigital.js.map +0 -1
- package/build/controls/InfoOnlyText.js +0 -35
- package/build/controls/InfoOnlyText.js.map +0 -1
- package/build/controls/Intercom.js.map +0 -1
- package/build/controls/Jalousie.js +0 -153
- package/build/controls/Jalousie.js.map +0 -1
- package/build/controls/LightController.js +0 -68
- package/build/controls/LightController.js.map +0 -1
- package/build/controls/LightControllerV2.js +0 -195
- package/build/controls/LightControllerV2.js.map +0 -1
- package/build/controls/MailBox.js +0 -41
- package/build/controls/MailBox.js.map +0 -1
- package/build/controls/Meter.js +0 -52
- package/build/controls/Meter.js.map +0 -1
- package/build/controls/None.js +0 -23
- package/build/controls/None.js.map +0 -1
- package/build/controls/PresenceDetector.js +0 -29
- package/build/controls/PresenceDetector.js.map +0 -1
- package/build/controls/Pushbutton.js +0 -35
- package/build/controls/Pushbutton.js.map +0 -1
- package/build/controls/Radio.js +0 -39
- package/build/controls/Radio.js.map +0 -1
- package/build/controls/Remote.js.map +0 -1
- package/build/controls/Slider.js +0 -44
- package/build/controls/Slider.js.map +0 -1
- package/build/controls/SmokeAlarm.js +0 -85
- package/build/controls/SmokeAlarm.js.map +0 -1
- package/build/controls/Switch.js +0 -31
- package/build/controls/Switch.js.map +0 -1
- package/build/controls/SystemScheme.js +0 -11
- package/build/controls/SystemScheme.js.map +0 -1
- package/build/controls/TextInput.js +0 -55
- package/build/controls/TextInput.js.map +0 -1
- package/build/controls/TextState.js +0 -20
- package/build/controls/TextState.js.map +0 -1
- package/build/controls/TimedSwitch.js +0 -36
- package/build/controls/TimedSwitch.js.map +0 -1
- package/build/controls/Tracker.js +0 -20
- package/build/controls/Tracker.js.map +0 -1
- package/build/controls/Unknown.js.map +0 -1
- package/build/controls/UpDownAnalog.js +0 -44
- package/build/controls/UpDownAnalog.js.map +0 -1
- package/build/controls/ValueSelector.js.map +0 -1
- package/build/controls/WindowMonitor.js +0 -84
- package/build/controls/WindowMonitor.js.map +0 -1
- package/build/controls/control-base.js +0 -12
- package/build/controls/control-base.js.map +0 -1
- package/build/loxone-handler-base.js +0 -186
- package/build/loxone-handler-base.js.map +0 -1
- package/build/main.js.map +0 -1
- package/build/weather-server-handler.js +0 -120
- package/build/weather-server-handler.js.map +0 -1
- package/doc/details-missing-control-type.png +0 -0
- package/doc/log-missing-control-type.png +0 -0
- package/doc/loxone-config-display-diagnostics.png +0 -0
- package/doc/loxone-config-info-only-digital.png +0 -0
- package/doc/loxone-config-use-in-visualization.png +0 -0
|
@@ -1,56 +1,150 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
|
2
|
+
import * as utils from '@iobroker/adapter-core';
|
|
3
|
+
import type * as SentryNode from '@sentry/node';
|
|
4
|
+
import type { EventProcessor } from '@sentry/types';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import FormData from 'form-data';
|
|
7
|
+
import * as LxCommunicator from 'lxcommunicator';
|
|
8
|
+
import Queue from 'queue-fifo';
|
|
9
|
+
import { v4 } from 'uuid';
|
|
10
|
+
import { Unknown } from './controls/Unknown';
|
|
11
|
+
import type { ControlBase, ControlType } from './controls/control-base';
|
|
12
|
+
import type {
|
|
13
|
+
Control,
|
|
14
|
+
Controls,
|
|
15
|
+
GlobalStates,
|
|
16
|
+
OperatingModes,
|
|
17
|
+
StructureFile,
|
|
18
|
+
WeatherServer,
|
|
19
|
+
} from './structure-file.ts';
|
|
20
|
+
import { WeatherServerHandler } from './weather-server-handler';
|
|
21
|
+
import { AllControls } from './controls/all-controls';
|
|
22
|
+
|
|
15
23
|
const WebSocketConfig = LxCommunicator.WebSocketConfig;
|
|
24
|
+
|
|
25
|
+
export type OldStateValue = ioBroker.StateValue | null | undefined;
|
|
26
|
+
export type CurrentStateValue = ioBroker.StateValue | null;
|
|
27
|
+
|
|
28
|
+
export type StateChangeListener = (oldValue: OldStateValue, newValue: CurrentStateValue) => void;
|
|
29
|
+
export type StateChangeListenerOpts = {
|
|
30
|
+
/** Don't call if new/old vals are the same & automatically ack state change */
|
|
31
|
+
notIfEqual?: boolean;
|
|
32
|
+
/** Convert state values to integers */
|
|
33
|
+
convertToInt?: boolean;
|
|
34
|
+
/** Values below minInt will be set to this value */
|
|
35
|
+
minInt?: number;
|
|
36
|
+
/** Values above maxInt will be set to this value */
|
|
37
|
+
maxInt?: number;
|
|
38
|
+
/** Override default timeout */
|
|
39
|
+
ackTimeoutMs?: number;
|
|
40
|
+
/** Acknowledge ourself (don't wait for Loxone) */
|
|
41
|
+
selfAck?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type StateChangeListenEntry = {
|
|
45
|
+
/** The listener function to call on state change */
|
|
46
|
+
listener: StateChangeListener;
|
|
47
|
+
/** Options for this listener */
|
|
48
|
+
opts?: StateChangeListenerOpts;
|
|
49
|
+
/** Queued value if a state change is in progress */
|
|
50
|
+
queuedVal: ioBroker.StateValue | null;
|
|
51
|
+
/** Timer for ack timeout */
|
|
52
|
+
ackTimer: ioBroker.Timeout | undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
16
55
|
// Log warnings if no ack event from Loxone in this time
|
|
17
56
|
// TODO: should this be configurable?
|
|
18
57
|
const ackTimeoutMs = 500;
|
|
58
|
+
|
|
19
59
|
// Period between connection attempts
|
|
20
60
|
const reconnectTimeoutMs = 5000;
|
|
21
|
-
|
|
22
|
-
|
|
61
|
+
|
|
62
|
+
export type StateEventHandler = (value: any) => Promise<void> | void;
|
|
63
|
+
export type StateEventRegistration = {
|
|
64
|
+
/** The name of the event handler used for removal */
|
|
65
|
+
name?: string;
|
|
66
|
+
/** The event handler function */
|
|
67
|
+
handler: StateEventHandler;
|
|
68
|
+
};
|
|
69
|
+
export type NamedStateEventHandler = (id: string, value: any) => Promise<void>;
|
|
70
|
+
export type LoxoneEvent = {
|
|
71
|
+
/** The UUID of the event */
|
|
72
|
+
uuid: string;
|
|
73
|
+
/** The event data */
|
|
74
|
+
evt: any;
|
|
75
|
+
};
|
|
76
|
+
export type Sentry = typeof SentryNode;
|
|
77
|
+
|
|
78
|
+
export type FormatInfoDetailsCallback = ((src: InfoDetailsEntryMap) => ioBroker.StateValue) | null;
|
|
79
|
+
export type InfoDetailsEntry = {
|
|
80
|
+
/** The event counter. */
|
|
81
|
+
count: number;
|
|
82
|
+
/** The last value received for this event. */
|
|
83
|
+
lastValue?: any;
|
|
84
|
+
};
|
|
85
|
+
export type InfoDetailsEntryMap = Map<string, InfoDetailsEntry>;
|
|
86
|
+
export type InfoEntry = {
|
|
87
|
+
/** The current state value */
|
|
88
|
+
value: ioBroker.StateValue;
|
|
89
|
+
/** The last set state value */
|
|
90
|
+
lastSet: ioBroker.StateValue;
|
|
91
|
+
/** Timer for state value reset */
|
|
92
|
+
timer: ioBroker.Timeout | undefined;
|
|
93
|
+
/** Map of detailed information entries */
|
|
94
|
+
detailsMap?: InfoDetailsEntryMap;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The Loxone adapter main class.
|
|
99
|
+
*/
|
|
100
|
+
export class Loxone extends utils.Adapter {
|
|
101
|
+
private uuid = '';
|
|
102
|
+
private socket?: any;
|
|
103
|
+
private existingObjects: Record<string, ioBroker.Object> = {};
|
|
104
|
+
private currentStateValues: Record<string, CurrentStateValue> = {};
|
|
105
|
+
private operatingModes: OperatingModes = {};
|
|
106
|
+
private foundRooms: Record<string, string[]> = {};
|
|
107
|
+
private foundCats: Record<string, string[]> = {};
|
|
108
|
+
|
|
109
|
+
private stateChangeListeners: Record<string, StateChangeListenEntry> = {};
|
|
110
|
+
private stateEventHandlers: Record<string, StateEventRegistration[]> = {};
|
|
111
|
+
|
|
112
|
+
private readonly eventsQueue = new Queue<LoxoneEvent>();
|
|
113
|
+
private runQueue = false;
|
|
114
|
+
private queueRunning = false;
|
|
115
|
+
|
|
116
|
+
public readonly reportedMissingControls = new Set<string>();
|
|
117
|
+
private readonly reportedUnsupportedStateChanges = new Set<string>();
|
|
118
|
+
private reconnectTimer?: ioBroker.Timeout;
|
|
119
|
+
private connectionInProgress = false;
|
|
120
|
+
private lxConnected = false;
|
|
121
|
+
|
|
122
|
+
private info: Map<string, InfoEntry>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates an instance of the Loxone adapter.
|
|
126
|
+
*
|
|
127
|
+
* @param options The adapter options.
|
|
128
|
+
*/
|
|
129
|
+
public constructor(options: Partial<utils.AdapterOptions> = {}) {
|
|
23
130
|
super({
|
|
24
|
-
dirname: __dirname.indexOf('node_modules') !== -1 ? undefined : __dirname
|
|
131
|
+
dirname: __dirname.indexOf('node_modules') !== -1 ? undefined : `${__dirname}/../`,
|
|
25
132
|
...options,
|
|
26
133
|
name: 'loxone',
|
|
27
134
|
});
|
|
28
|
-
this.uuid = '';
|
|
29
|
-
this.existingObjects = {};
|
|
30
|
-
this.currentStateValues = {};
|
|
31
|
-
this.operatingModes = {};
|
|
32
|
-
this.foundRooms = {};
|
|
33
|
-
this.foundCats = {};
|
|
34
|
-
this.stateChangeListeners = {};
|
|
35
|
-
this.stateEventHandlers = {};
|
|
36
|
-
this.eventsQueue = new Queue();
|
|
37
|
-
this.runQueue = false;
|
|
38
|
-
this.queueRunning = false;
|
|
39
|
-
this.reportedMissingControls = new Set();
|
|
40
|
-
this.reportedUnsupportedStateChanges = new Set();
|
|
41
|
-
this.connectionInProgress = false;
|
|
42
|
-
this.lxConnected = false;
|
|
43
135
|
this.on('ready', this.onReady.bind(this));
|
|
44
136
|
this.on('stateChange', this.onStateChange.bind(this));
|
|
45
137
|
this.on('unload', this.onUnload.bind(this));
|
|
46
|
-
this.info = new Map();
|
|
138
|
+
this.info = new Map<string, InfoEntry>();
|
|
47
139
|
}
|
|
140
|
+
|
|
48
141
|
/**
|
|
49
142
|
* Is called when databases are connected and adapter received configuration.
|
|
50
143
|
*/
|
|
51
|
-
async onReady() {
|
|
144
|
+
private async onReady(): Promise<void> {
|
|
52
145
|
// Init info
|
|
53
146
|
await this.initInfoStates();
|
|
147
|
+
|
|
54
148
|
// store all current (acknowledged) state values
|
|
55
149
|
const allStates = await this.getStatesAsync('*');
|
|
56
150
|
for (const id in allStates) {
|
|
@@ -58,47 +152,58 @@ class Loxone extends utils.Adapter {
|
|
|
58
152
|
this.currentStateValues[id] = allStates[id].val;
|
|
59
153
|
}
|
|
60
154
|
}
|
|
155
|
+
|
|
61
156
|
// store all existing objects for later use
|
|
62
157
|
this.existingObjects = await this.getAdapterObjectsAsync();
|
|
158
|
+
|
|
63
159
|
// Reset the connection indicator during startup
|
|
64
160
|
this.setConnectionState(false);
|
|
65
|
-
this.uuid =
|
|
161
|
+
this.uuid = v4();
|
|
66
162
|
// connect to Loxone Miniserver
|
|
67
|
-
const webSocketConfig = new WebSocketConfig(
|
|
68
|
-
|
|
163
|
+
const webSocketConfig = new WebSocketConfig(
|
|
164
|
+
WebSocketConfig.protocol.WS,
|
|
165
|
+
this.uuid,
|
|
166
|
+
'iobroker',
|
|
167
|
+
WebSocketConfig.permission.APP,
|
|
168
|
+
false,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const handleAnyEvent = (uuid: string, evt: any): void => {
|
|
69
172
|
this.log.silly(`received update event: ${JSON.stringify(evt)}: ${uuid}`);
|
|
70
173
|
this.eventsQueue.enqueue({ uuid, evt });
|
|
71
|
-
this.handleEventQueue().catch(
|
|
72
|
-
var _a;
|
|
174
|
+
this.handleEventQueue().catch(e => {
|
|
73
175
|
this.log.error(`Unhandled error in event ${uuid}: ${e}`);
|
|
74
|
-
|
|
176
|
+
this.getSentry()?.captureException(e, { extra: { uuid, evt } });
|
|
75
177
|
});
|
|
76
178
|
};
|
|
179
|
+
|
|
77
180
|
webSocketConfig.delegate = {
|
|
78
|
-
socketOnDataProgress: (socket, progress) => {
|
|
79
|
-
this.log.debug(
|
|
181
|
+
socketOnDataProgress: (socket: any, progress: any) => {
|
|
182
|
+
this.log.debug(`data progress ${progress}`);
|
|
80
183
|
},
|
|
81
|
-
socketOnTokenConfirmed: (_socket, _response) => {
|
|
184
|
+
socketOnTokenConfirmed: (_socket: any, _response: any) => {
|
|
82
185
|
this.log.debug('token confirmed');
|
|
83
186
|
},
|
|
84
|
-
socketOnTokenReceived: (_socket, _result) => {
|
|
187
|
+
socketOnTokenReceived: (_socket: any, _result: any) => {
|
|
85
188
|
this.log.debug('token received');
|
|
86
189
|
},
|
|
87
|
-
socketOnConnectionClosed: (socket, code) => {
|
|
88
|
-
this.log.info(
|
|
190
|
+
socketOnConnectionClosed: (socket: any, code: string) => {
|
|
191
|
+
this.log.info(`Socket closed ${code}`);
|
|
89
192
|
this.setConnectionState(false);
|
|
193
|
+
|
|
90
194
|
// Stop queue and clear it. Issue a warning if it isn't empty.
|
|
91
195
|
this.runQueue = false;
|
|
92
196
|
if (this.eventsQueue.size() > 0) {
|
|
93
|
-
this.log.warn(
|
|
197
|
+
this.log.warn(`Event queue is not empty. Discarding ${this.eventsQueue.size()} items`);
|
|
94
198
|
}
|
|
95
199
|
// Yes - I know this could go in the 'if' above but here 'just in case' ;)
|
|
96
200
|
this.eventsQueue.clear();
|
|
201
|
+
|
|
97
202
|
if (code != LxCommunicator.SupportCode.WEBSOCKET_MANUAL_CLOSE) {
|
|
98
203
|
this.reconnect();
|
|
99
204
|
}
|
|
100
205
|
},
|
|
101
|
-
socketOnEventReceived: (socket, events, type) => {
|
|
206
|
+
socketOnEventReceived: (socket: any, events: any, type: number) => {
|
|
102
207
|
this.log.silly(`socket event received ${type} ${JSON.stringify(events)}`);
|
|
103
208
|
this.incInfoState('info.messagesReceived');
|
|
104
209
|
for (const evt of events) {
|
|
@@ -109,9 +214,6 @@ class Loxone extends utils.Adapter {
|
|
|
109
214
|
case LxCommunicator.BinaryEvent.Type.EVENTTEXT:
|
|
110
215
|
handleAnyEvent(evt.uuid, evt.text);
|
|
111
216
|
break;
|
|
112
|
-
case LxCommunicator.BinaryEvent.Type.EVENT:
|
|
113
|
-
handleAnyEvent(evt.uuid, evt);
|
|
114
|
-
break;
|
|
115
217
|
case LxCommunicator.BinaryEvent.Type.WEATHER:
|
|
116
218
|
handleAnyEvent(evt.uuid, evt);
|
|
117
219
|
break;
|
|
@@ -122,16 +224,18 @@ class Loxone extends utils.Adapter {
|
|
|
122
224
|
},
|
|
123
225
|
};
|
|
124
226
|
this.socket = new LxCommunicator.WebSocket(webSocketConfig);
|
|
227
|
+
|
|
125
228
|
await this.connect();
|
|
229
|
+
|
|
126
230
|
this.subscribeStates('*');
|
|
127
231
|
}
|
|
128
|
-
|
|
129
|
-
|
|
232
|
+
|
|
233
|
+
private async loadStructureFile(): Promise<boolean> {
|
|
234
|
+
let file: StructureFile;
|
|
130
235
|
try {
|
|
131
236
|
const fileString = await this.socket.send('data/LoxAPP3.json');
|
|
132
237
|
file = JSON.parse(fileString);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
238
|
+
} catch {
|
|
135
239
|
// do not stringify error, it can contain circular references
|
|
136
240
|
this.log.error(`Couldn't get structure file`);
|
|
137
241
|
return false;
|
|
@@ -141,205 +245,235 @@ class Loxone extends utils.Adapter {
|
|
|
141
245
|
const sentry = this.getSentry();
|
|
142
246
|
if (sentry) {
|
|
143
247
|
// add a global event processor to upload the structure file (only once)
|
|
144
|
-
sentry.
|
|
248
|
+
sentry.addEventProcessor(this.createSentryEventProcessor(file));
|
|
145
249
|
}
|
|
250
|
+
|
|
146
251
|
try {
|
|
147
252
|
await this.loadStructureFileAsync(file);
|
|
148
253
|
this.log.debug('structure file successfully loaded');
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
254
|
+
} catch (error) {
|
|
151
255
|
// do not stringify error, it can contain circular references
|
|
152
256
|
this.log.error(`Couldn't load structure file`);
|
|
153
|
-
sentry
|
|
257
|
+
sentry?.captureException(error, { extra: { file } });
|
|
154
258
|
return false;
|
|
155
259
|
}
|
|
260
|
+
|
|
156
261
|
return true; // Success
|
|
157
262
|
}
|
|
158
|
-
|
|
263
|
+
|
|
264
|
+
private async connect(): Promise<void> {
|
|
159
265
|
if (this.connectionInProgress) {
|
|
160
266
|
this.log.warn('Connection already in progress');
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
267
|
+
} else {
|
|
163
268
|
this.log.info('Trying to connect');
|
|
164
269
|
this.connectionInProgress = true;
|
|
270
|
+
|
|
165
271
|
let success = true; // Assume success
|
|
272
|
+
|
|
166
273
|
try {
|
|
167
|
-
await this.socket.open(
|
|
168
|
-
|
|
169
|
-
|
|
274
|
+
await this.socket.open(
|
|
275
|
+
`${this.config.host}:${this.config.port}`,
|
|
276
|
+
this.config.username,
|
|
277
|
+
this.config.password,
|
|
278
|
+
);
|
|
279
|
+
} catch {
|
|
170
280
|
// do not stringify error, it can contain circular references
|
|
171
281
|
this.log.error(`Couldn't open socket`);
|
|
172
282
|
success = false;
|
|
173
283
|
}
|
|
284
|
+
|
|
174
285
|
if (success) {
|
|
175
286
|
success = await this.loadStructureFile();
|
|
176
287
|
}
|
|
288
|
+
|
|
177
289
|
if (success) {
|
|
178
290
|
try {
|
|
179
291
|
await this.socket.send('jdev/sps/enablebinstatusupdate');
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
292
|
+
} catch {
|
|
182
293
|
// do not stringify error, it can contain circular references
|
|
183
294
|
this.log.error(`Couldn't enable status updates`);
|
|
184
295
|
success = false;
|
|
185
296
|
}
|
|
186
297
|
}
|
|
298
|
+
|
|
187
299
|
this.connectionInProgress = false;
|
|
300
|
+
|
|
188
301
|
if (!success) {
|
|
189
302
|
this.log.debug('Connection failed - will retry after delay');
|
|
190
303
|
this.socket.close();
|
|
191
304
|
this.reconnect();
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
305
|
+
} else {
|
|
194
306
|
// We are ready, let's set the connection indicator
|
|
195
307
|
this.setConnectionState(true);
|
|
196
308
|
}
|
|
197
309
|
}
|
|
198
310
|
}
|
|
199
|
-
|
|
311
|
+
|
|
312
|
+
private reconnect(): void {
|
|
200
313
|
if (this.reconnectTimer) {
|
|
201
314
|
this.log.debug('Reconnect called while timer already running');
|
|
202
|
-
}
|
|
203
|
-
else if (this.connectionInProgress) {
|
|
315
|
+
} else if (this.connectionInProgress) {
|
|
204
316
|
this.log.debug('Reconnect called while connection in progress');
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
317
|
+
} else {
|
|
207
318
|
this.reconnectTimer = this.setTimeout(() => {
|
|
208
319
|
delete this.reconnectTimer;
|
|
209
|
-
this.connect().catch(
|
|
320
|
+
this.connect().catch(e => {
|
|
210
321
|
this.log.error(`Couldn't reconnect: ${e}`);
|
|
211
322
|
this.reconnect();
|
|
212
323
|
});
|
|
213
324
|
}, reconnectTimeoutMs);
|
|
214
325
|
}
|
|
215
326
|
}
|
|
216
|
-
|
|
327
|
+
|
|
328
|
+
private setConnectionState(connected: boolean): void {
|
|
217
329
|
this.lxConnected = connected;
|
|
218
|
-
this.setState('info.connection', this.lxConnected, true);
|
|
330
|
+
this.setState('info.connection', this.lxConnected, true).catch(e => this.log.warn(e));
|
|
219
331
|
}
|
|
332
|
+
|
|
220
333
|
/**
|
|
221
334
|
* Is called when adapter shuts down - callback has to be called under any circumstances!
|
|
335
|
+
*
|
|
336
|
+
* @param callback the callback to call when cleanup is done
|
|
222
337
|
*/
|
|
223
|
-
onUnload(callback) {
|
|
338
|
+
private onUnload(callback: () => void): void {
|
|
224
339
|
try {
|
|
225
340
|
if (this.socket) {
|
|
226
341
|
this.socket.close();
|
|
227
342
|
delete this.socket;
|
|
228
343
|
}
|
|
229
344
|
callback();
|
|
230
|
-
}
|
|
231
|
-
catch (e) {
|
|
345
|
+
} catch {
|
|
232
346
|
callback();
|
|
233
347
|
}
|
|
234
348
|
this.flushInfoStates();
|
|
235
349
|
// TODO: clear queued state change timers
|
|
236
350
|
}
|
|
351
|
+
|
|
237
352
|
/**
|
|
238
|
-
* Is called if a subscribed state changes
|
|
353
|
+
* Is called if a subscribed state changes.
|
|
354
|
+
*
|
|
355
|
+
* @param id the id of the state
|
|
356
|
+
* @param state the state object
|
|
239
357
|
*/
|
|
240
|
-
async onStateChange(id, state) {
|
|
358
|
+
private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {
|
|
241
359
|
// Warning: state can be null if it was deleted!
|
|
242
360
|
if (!id || !state || state.ack) {
|
|
243
361
|
// Do nothing
|
|
244
|
-
}
|
|
245
|
-
else if (id.includes('.info.')) {
|
|
362
|
+
} else if (id.includes('.info.')) {
|
|
246
363
|
// Ignore info changes
|
|
247
364
|
// TODO: can this be done better by ignoring '.info.' in subscribeStates?
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
365
|
+
} else {
|
|
250
366
|
this.log.debug(`stateChange ${id} ${JSON.stringify(state)}`);
|
|
251
|
-
if (!this.stateChangeListeners
|
|
252
|
-
const msg =
|
|
367
|
+
if (!(id in this.stateChangeListeners)) {
|
|
368
|
+
const msg = `Unsupported state change: ${id}`;
|
|
253
369
|
this.log.error(msg);
|
|
254
370
|
if (!this.reportedUnsupportedStateChanges.has(id)) {
|
|
255
371
|
this.reportedUnsupportedStateChanges.add(id);
|
|
256
372
|
const sentry = this.getSentry();
|
|
257
|
-
sentry
|
|
373
|
+
sentry?.withScope(scope => {
|
|
258
374
|
scope.setExtra('state', state);
|
|
259
375
|
sentry.captureMessage(msg, 'warning');
|
|
260
376
|
});
|
|
261
377
|
}
|
|
262
|
-
}
|
|
263
|
-
else if (!this.lxConnected) {
|
|
378
|
+
} else if (!this.lxConnected) {
|
|
264
379
|
this.log.warn(`stateChange ${id} while disconnected, discarding`);
|
|
265
380
|
this.incInfoState('info.stateChangesDiscarded');
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
381
|
+
} else {
|
|
268
382
|
const stateChangeListener = this.stateChangeListeners[id];
|
|
269
383
|
if (stateChangeListener.ackTimer) {
|
|
270
384
|
// Ack timer is running: we didn't get a reply from the previous command yet
|
|
271
385
|
if (stateChangeListener.queuedVal !== null) {
|
|
272
386
|
// Already a queued state change: we're going to have to discard that and replace with latest
|
|
273
|
-
this.log.warn(
|
|
387
|
+
this.log.warn(
|
|
388
|
+
`State change in progress for ${id}, discarding ${stateChangeListener.queuedVal}`,
|
|
389
|
+
);
|
|
274
390
|
this.incInfoState('info.stateChangesDiscarded');
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
391
|
+
} else {
|
|
277
392
|
// Nothing queued, so this will only be delayed (at least for now)
|
|
278
393
|
this.log.warn(`State change in progress for ${id}, delaying ${state.val}`);
|
|
279
394
|
this.incInfoState('info.stateChangesDelayed');
|
|
280
395
|
}
|
|
281
396
|
stateChangeListener.queuedVal = state.val;
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
397
|
+
} else {
|
|
284
398
|
// Ack timer is not running, so we're all good to handle this
|
|
285
399
|
await this.handleStateChange(id, stateChangeListener, state.val);
|
|
286
400
|
}
|
|
287
401
|
}
|
|
288
402
|
}
|
|
289
403
|
}
|
|
290
|
-
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Convert a state value to an integer.
|
|
407
|
+
*
|
|
408
|
+
* @param value The state value to convert.
|
|
409
|
+
* @returns The integer representation of the state value.
|
|
410
|
+
*/
|
|
411
|
+
public convertStateToInt(value: OldStateValue): number {
|
|
291
412
|
return !value ? 0 : parseInt(value.toString());
|
|
292
413
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
414
|
+
|
|
415
|
+
private async handleStateChange(
|
|
416
|
+
id: string,
|
|
417
|
+
stateChangeListener: StateChangeListenEntry,
|
|
418
|
+
val: ioBroker.StateValue,
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
if (stateChangeListener.opts?.convertToInt) {
|
|
296
421
|
// Convert any values to ints within range if necessary.
|
|
297
422
|
val = this.convertStateToInt(val);
|
|
298
|
-
if (
|
|
423
|
+
if (stateChangeListener.opts?.minInt !== undefined && val < stateChangeListener.opts.minInt) {
|
|
299
424
|
val = stateChangeListener.opts.minInt;
|
|
300
425
|
}
|
|
301
|
-
if (
|
|
426
|
+
if (stateChangeListener.opts?.maxInt !== undefined && val > stateChangeListener.opts.maxInt) {
|
|
302
427
|
val = stateChangeListener.opts.maxInt;
|
|
303
428
|
}
|
|
304
429
|
}
|
|
305
|
-
if (
|
|
430
|
+
if (stateChangeListener.opts?.notIfEqual && this.currentStateValues[id] === val) {
|
|
306
431
|
// new/old values are the same so don't send update.
|
|
307
432
|
// However, ack the state change as we have 'handled' this (by doing nothing)
|
|
308
433
|
this.log.debug(`State value is unchanged, no listener+self-ack: ${id} ${val}`);
|
|
309
434
|
await this.setStateAck(id, val);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (!((_e = stateChangeListener.opts) === null || _e === void 0 ? void 0 : _e.selfAck)) {
|
|
435
|
+
} else {
|
|
436
|
+
if (!stateChangeListener.opts?.selfAck) {
|
|
313
437
|
// Set ack timer before calling listener
|
|
314
|
-
stateChangeListener.ackTimer = this.setTimeout(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
stateChangeListener
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
438
|
+
stateChangeListener.ackTimer = this.setTimeout(
|
|
439
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
440
|
+
// @ts-ignore
|
|
441
|
+
async (id: string, stateChangeListener: StateChangeListenEntry) => {
|
|
442
|
+
this.log.warn(`Timeout for ack ${id}`);
|
|
443
|
+
this.incInfoState('info.ackTimeouts', id);
|
|
444
|
+
stateChangeListener.ackTimer = undefined;
|
|
445
|
+
// Even though this is a timeout, handle any change that may have been delayed waiting for this
|
|
446
|
+
await this.handleDelayedStateChange(id, stateChangeListener);
|
|
447
|
+
},
|
|
448
|
+
stateChangeListener.opts?.ackTimeoutMs ? stateChangeListener.opts?.ackTimeoutMs : ackTimeoutMs,
|
|
449
|
+
id,
|
|
450
|
+
stateChangeListener,
|
|
451
|
+
);
|
|
321
452
|
}
|
|
453
|
+
|
|
322
454
|
// Change will be handled by listener
|
|
323
455
|
stateChangeListener.listener(this.currentStateValues[id], val);
|
|
324
|
-
|
|
456
|
+
|
|
457
|
+
if (stateChangeListener.opts?.selfAck) {
|
|
325
458
|
// Loxone is not expected to send an event to acknowledge this so just do it ourself
|
|
326
459
|
this.log.debug(`Self-ack: ${id} ${val}`);
|
|
327
460
|
await this.setStateAck(id, val);
|
|
328
461
|
}
|
|
329
462
|
}
|
|
330
463
|
}
|
|
331
|
-
|
|
464
|
+
|
|
465
|
+
private async handleDelayedStateChange(id: string, stateChangeListener: StateChangeListenEntry): Promise<void> {
|
|
332
466
|
if (stateChangeListener.queuedVal !== null) {
|
|
333
467
|
this.log.debug(`Handling delayed state: ${id} ${stateChangeListener.queuedVal}`);
|
|
334
468
|
await this.handleStateChange(id, stateChangeListener, stateChangeListener.queuedVal);
|
|
335
469
|
stateChangeListener.queuedVal = null;
|
|
336
470
|
}
|
|
337
471
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
472
|
+
|
|
473
|
+
private createSentryEventProcessor(data: StructureFile): EventProcessor {
|
|
474
|
+
const sentry = this.getSentry()!;
|
|
475
|
+
let attachmentEventId: string | undefined;
|
|
476
|
+
return async (event: SentryNode.Event) => {
|
|
343
477
|
try {
|
|
344
478
|
if (attachmentEventId) {
|
|
345
479
|
// structure file was already added
|
|
@@ -353,28 +487,34 @@ class Loxone extends utils.Adapter {
|
|
|
353
487
|
}
|
|
354
488
|
return event;
|
|
355
489
|
}
|
|
356
|
-
const dsn =
|
|
490
|
+
const dsn = sentry.getCurrentHub().getClient()?.getDsn();
|
|
357
491
|
if (!dsn || !event.event_id) {
|
|
358
492
|
return event;
|
|
359
493
|
}
|
|
494
|
+
|
|
360
495
|
attachmentEventId = event.event_id;
|
|
496
|
+
|
|
361
497
|
const { host, path, projectId, port, protocol, publicKey } = dsn;
|
|
362
|
-
const endpoint = `${protocol}://${host}${port !== '' ? `:${port}` : ''}${
|
|
498
|
+
const endpoint = `${protocol}://${host}${port !== '' ? `:${port}` : ''}${
|
|
499
|
+
path !== '' ? `/${path}` : ''
|
|
500
|
+
}/api/${projectId}/events/${attachmentEventId}/attachments/?sentry_key=${publicKey}&sentry_version=7&sentry_client=custom-javascript`;
|
|
501
|
+
|
|
363
502
|
const form = new FormData();
|
|
364
503
|
form.append('att', JSON.stringify(data, null, 2), {
|
|
365
504
|
contentType: 'application/json',
|
|
366
505
|
filename: 'LoxAPP3.json',
|
|
367
506
|
});
|
|
368
|
-
await
|
|
507
|
+
await axios.post(endpoint, form, { headers: form.getHeaders() });
|
|
369
508
|
return event;
|
|
370
|
-
}
|
|
371
|
-
catch (ex) {
|
|
509
|
+
} catch (ex: any) {
|
|
372
510
|
this.log.error(`Couldn't upload structure file attachment to sentry: ${ex}`);
|
|
373
511
|
}
|
|
512
|
+
|
|
374
513
|
return event;
|
|
375
514
|
};
|
|
376
515
|
}
|
|
377
|
-
|
|
516
|
+
|
|
517
|
+
private async loadStructureFileAsync(data: StructureFile): Promise<void> {
|
|
378
518
|
this.stateEventHandlers = {};
|
|
379
519
|
this.foundRooms = {};
|
|
380
520
|
this.foundCats = {};
|
|
@@ -384,12 +524,19 @@ class Loxone extends utils.Adapter {
|
|
|
384
524
|
await this.loadEnumsAsync(data.rooms, 'enum.rooms', this.foundRooms, this.config.syncRooms);
|
|
385
525
|
await this.loadEnumsAsync(data.cats, 'enum.functions', this.foundCats, this.config.syncFunctions);
|
|
386
526
|
await this.loadWeatherServerAsync(data.weatherServer);
|
|
527
|
+
|
|
387
528
|
// replay all queued events
|
|
388
529
|
this.runQueue = true;
|
|
389
530
|
await this.handleEventQueue();
|
|
390
531
|
}
|
|
391
|
-
|
|
392
|
-
|
|
532
|
+
|
|
533
|
+
private async loadGlobalStatesAsync(globalStates: GlobalStates): Promise<void> {
|
|
534
|
+
interface GlobalStateInfo {
|
|
535
|
+
type: ioBroker.CommonType;
|
|
536
|
+
role: string;
|
|
537
|
+
handler: (name: string, value: ioBroker.StateValue) => Promise<void>;
|
|
538
|
+
}
|
|
539
|
+
const globalStateInfos: Record<string, GlobalStateInfo> = {
|
|
393
540
|
operatingMode: {
|
|
394
541
|
type: 'number',
|
|
395
542
|
role: 'value',
|
|
@@ -421,11 +568,12 @@ class Loxone extends utils.Adapter {
|
|
|
421
568
|
handler: (name, value) => this.setStateAck(name, value === 1),
|
|
422
569
|
},
|
|
423
570
|
};
|
|
424
|
-
const defaultInfo = {
|
|
571
|
+
const defaultInfo: GlobalStateInfo = {
|
|
425
572
|
type: 'string',
|
|
426
573
|
role: 'text',
|
|
427
574
|
handler: (name, value) => this.setStateAck(name, `${value}`),
|
|
428
575
|
};
|
|
576
|
+
|
|
429
577
|
// special case for operating mode (text)
|
|
430
578
|
await this.updateObjectAsync('operatingMode-text', {
|
|
431
579
|
type: 'state',
|
|
@@ -440,37 +588,43 @@ class Loxone extends utils.Adapter {
|
|
|
440
588
|
uuid: globalStates.operatingMode,
|
|
441
589
|
},
|
|
442
590
|
});
|
|
591
|
+
|
|
443
592
|
for (const globalStateName in globalStates) {
|
|
444
|
-
const info = globalStateInfos
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
593
|
+
const info = globalStateInfos[globalStateName] ?? defaultInfo;
|
|
594
|
+
await this.updateStateObjectAsync(
|
|
595
|
+
globalStateName,
|
|
596
|
+
{
|
|
597
|
+
name: globalStateName,
|
|
598
|
+
read: true,
|
|
599
|
+
write: false,
|
|
600
|
+
type: info.type,
|
|
601
|
+
role: info.role,
|
|
602
|
+
},
|
|
603
|
+
globalStates[globalStateName],
|
|
604
|
+
info.handler,
|
|
605
|
+
);
|
|
454
606
|
}
|
|
455
607
|
}
|
|
456
|
-
|
|
608
|
+
|
|
609
|
+
private async setOperatingMode(name: string, value: any): Promise<void> {
|
|
457
610
|
await this.setStateAck(name, value);
|
|
458
|
-
await this.setStateAck(name
|
|
611
|
+
await this.setStateAck(`${name}-text`, this.operatingModes[value]);
|
|
459
612
|
}
|
|
460
|
-
|
|
461
|
-
|
|
613
|
+
|
|
614
|
+
private async loadControlsAsync(controls: Controls): Promise<void> {
|
|
462
615
|
let hasUnsupported = false;
|
|
463
616
|
for (const uuid in controls) {
|
|
464
617
|
const control = controls[uuid];
|
|
465
|
-
if (!
|
|
618
|
+
if (!('type' in control)) {
|
|
466
619
|
continue;
|
|
467
620
|
}
|
|
621
|
+
|
|
468
622
|
try {
|
|
469
623
|
await this.loadControlAsync('device', uuid, control);
|
|
470
|
-
}
|
|
471
|
-
catch (e) {
|
|
624
|
+
} catch (e: any) {
|
|
472
625
|
this.log.info(`Currently unsupported control type ${control.type}: ${e}`);
|
|
473
|
-
|
|
626
|
+
this.getSentry()?.captureException(e, { extra: { uuid, control } });
|
|
627
|
+
|
|
474
628
|
if (!hasUnsupported) {
|
|
475
629
|
hasUnsupported = true;
|
|
476
630
|
await this.updateObjectAsync('Unsupported', {
|
|
@@ -482,7 +636,8 @@ class Loxone extends utils.Adapter {
|
|
|
482
636
|
native: {},
|
|
483
637
|
});
|
|
484
638
|
}
|
|
485
|
-
|
|
639
|
+
|
|
640
|
+
await this.updateObjectAsync(`Unsupported.${uuid}`, {
|
|
486
641
|
type: 'state',
|
|
487
642
|
common: {
|
|
488
643
|
name: control.name,
|
|
@@ -496,73 +651,93 @@ class Loxone extends utils.Adapter {
|
|
|
496
651
|
}
|
|
497
652
|
}
|
|
498
653
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Load sub-controls of a control.
|
|
657
|
+
*
|
|
658
|
+
* @param parentUuid The UUID of the parent control.
|
|
659
|
+
* @param control The control containing sub-controls.
|
|
660
|
+
* @returns A promise that resolves when sub-controls are loaded.
|
|
661
|
+
*/
|
|
662
|
+
public async loadSubControlsAsync(parentUuid: string, control: Control): Promise<void> {
|
|
663
|
+
if (!('subControls' in control)) {
|
|
502
664
|
return;
|
|
503
665
|
}
|
|
504
666
|
for (let uuid in control.subControls) {
|
|
505
667
|
const subControl = control.subControls[uuid];
|
|
506
|
-
if (!
|
|
668
|
+
if (!('type' in subControl)) {
|
|
507
669
|
continue;
|
|
508
670
|
}
|
|
671
|
+
|
|
509
672
|
try {
|
|
510
|
-
if (uuid.startsWith(parentUuid
|
|
673
|
+
if (uuid.startsWith(`${parentUuid}/`)) {
|
|
511
674
|
uuid = uuid.replace('/', '.');
|
|
675
|
+
} else {
|
|
676
|
+
uuid = `${parentUuid}.${uuid.replace('/', '-')}`;
|
|
512
677
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
subControl.name = control.name + ': ' + subControl.name;
|
|
678
|
+
subControl.name = `${control.name}: ${subControl.name}`;
|
|
679
|
+
|
|
517
680
|
await this.loadControlAsync('channel', uuid, subControl);
|
|
518
|
-
}
|
|
519
|
-
catch (e) {
|
|
681
|
+
} catch (e: any) {
|
|
520
682
|
this.log.info(`Currently unsupported sub-control type ${subControl.type}: ${e}`);
|
|
521
|
-
|
|
683
|
+
this.getSentry()?.captureException(e, { extra: { uuid, subControl } });
|
|
522
684
|
}
|
|
523
685
|
}
|
|
524
686
|
}
|
|
525
|
-
|
|
526
|
-
|
|
687
|
+
|
|
688
|
+
private async loadControlAsync(controlType: ControlType, uuid: string, control: Control): Promise<void> {
|
|
527
689
|
const type = control.type || 'None';
|
|
528
690
|
if (type.match(/[^a-z0-9]/i)) {
|
|
529
691
|
throw new Error(`Bad control type: ${type}`);
|
|
530
692
|
}
|
|
531
|
-
|
|
693
|
+
|
|
694
|
+
let controlObject: ControlBase;
|
|
532
695
|
try {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
controlObject = new Unknown_1.Unknown(this);
|
|
696
|
+
controlObject = new AllControls[type as keyof typeof AllControls](this);
|
|
697
|
+
} catch (e: any) {
|
|
698
|
+
this.log.silly(`Couldn't load control type ${type}: ${e}`);
|
|
699
|
+
controlObject = new Unknown(this);
|
|
538
700
|
}
|
|
539
701
|
await controlObject.loadAsync(controlType, uuid, control);
|
|
540
|
-
|
|
541
|
-
|
|
702
|
+
|
|
703
|
+
if ('room' in control) {
|
|
704
|
+
if (!this.foundRooms[control.room]) {
|
|
542
705
|
this.foundRooms[control.room] = [];
|
|
543
706
|
}
|
|
707
|
+
|
|
544
708
|
this.foundRooms[control.room].push(uuid);
|
|
545
709
|
}
|
|
546
|
-
|
|
547
|
-
|
|
710
|
+
|
|
711
|
+
if ('cat' in control) {
|
|
712
|
+
if (!this.foundCats[control.cat]) {
|
|
548
713
|
this.foundCats[control.cat] = [];
|
|
549
714
|
}
|
|
715
|
+
|
|
550
716
|
this.foundCats[control.cat].push(uuid);
|
|
551
717
|
}
|
|
552
718
|
}
|
|
553
|
-
|
|
719
|
+
|
|
720
|
+
private async loadEnumsAsync(
|
|
721
|
+
values: Record<string, any>,
|
|
722
|
+
enumName: string,
|
|
723
|
+
found: Record<string, string[]>,
|
|
724
|
+
enabled: boolean,
|
|
725
|
+
): Promise<void> {
|
|
554
726
|
if (!enabled) {
|
|
555
727
|
return;
|
|
556
728
|
}
|
|
729
|
+
|
|
557
730
|
for (const uuid in values) {
|
|
558
|
-
if (!found
|
|
731
|
+
if (!found[uuid]) {
|
|
559
732
|
// don't sync room/cat if we have no control that is using it
|
|
560
733
|
continue;
|
|
561
734
|
}
|
|
735
|
+
|
|
562
736
|
const members = [];
|
|
563
|
-
for (const
|
|
564
|
-
members.push(this.namespace
|
|
737
|
+
for (const id of found[uuid]) {
|
|
738
|
+
members.push(`${this.namespace}.${id}`);
|
|
565
739
|
}
|
|
740
|
+
|
|
566
741
|
const item = values[uuid];
|
|
567
742
|
const name = item.name.replace(/[\][*.,;'"`<>\\?]+/g, '_');
|
|
568
743
|
const obj = {
|
|
@@ -573,19 +748,20 @@ class Loxone extends utils.Adapter {
|
|
|
573
748
|
},
|
|
574
749
|
native: item,
|
|
575
750
|
};
|
|
576
|
-
|
|
751
|
+
|
|
752
|
+
await this.updateEnumObjectAsync(`${enumName}.${name}`, obj);
|
|
577
753
|
}
|
|
578
754
|
}
|
|
579
|
-
|
|
755
|
+
|
|
756
|
+
private async updateEnumObjectAsync(id: string, newObj: any): Promise<void> {
|
|
580
757
|
// TODO: the parameter newObj should be an EnumObject, but currently that doesn't exist in the type definition
|
|
581
758
|
// similar to hm-rega.js:
|
|
582
|
-
let obj = await this.getForeignObjectAsync(id);
|
|
759
|
+
let obj: any = await this.getForeignObjectAsync(id);
|
|
583
760
|
let changed = false;
|
|
584
761
|
if (!obj) {
|
|
585
762
|
obj = newObj;
|
|
586
763
|
changed = true;
|
|
587
|
-
}
|
|
588
|
-
else if (newObj && newObj.common && newObj.common.members) {
|
|
764
|
+
} else if (newObj && newObj.common && newObj.common.members) {
|
|
589
765
|
obj.common = obj.common || {};
|
|
590
766
|
obj.common.members = obj.common.members || [];
|
|
591
767
|
for (let m = 0; m < newObj.common.members.length; m++) {
|
|
@@ -599,27 +775,27 @@ class Loxone extends utils.Adapter {
|
|
|
599
775
|
await this.setForeignObjectAsync(id, obj);
|
|
600
776
|
}
|
|
601
777
|
}
|
|
602
|
-
|
|
778
|
+
|
|
779
|
+
private async loadWeatherServerAsync(data: WeatherServer): Promise<void> {
|
|
603
780
|
if (this.config.weatherServer === 'off') {
|
|
604
781
|
this.log.debug('WeatherServer is disabled in the adapter configuration');
|
|
605
782
|
return;
|
|
606
783
|
}
|
|
607
|
-
const handler = new
|
|
784
|
+
const handler = new WeatherServerHandler(this);
|
|
608
785
|
await handler.loadAsync(data, this.config.weatherServer || 'all');
|
|
609
786
|
}
|
|
610
|
-
|
|
787
|
+
|
|
788
|
+
private async handleEventQueue(): Promise<void> {
|
|
611
789
|
// TODO: This solution with globals for runQueue & queueRunning
|
|
612
790
|
// isn't very elegant. It works, but is there a better way?
|
|
613
791
|
if (!this.runQueue) {
|
|
614
792
|
this.log.silly('Asked to handle the queue, but is stopped');
|
|
615
|
-
}
|
|
616
|
-
else if (this.queueRunning) {
|
|
793
|
+
} else if (this.queueRunning) {
|
|
617
794
|
this.log.silly('Asked to handle the queue, but already in progress');
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
795
|
+
} else {
|
|
620
796
|
this.queueRunning = true;
|
|
621
|
-
this.log.silly(
|
|
622
|
-
let evt;
|
|
797
|
+
this.log.silly(`Processing events from queue length: ${this.eventsQueue.size()}`);
|
|
798
|
+
let evt: LoxoneEvent | null;
|
|
623
799
|
while ((evt = this.eventsQueue.dequeue())) {
|
|
624
800
|
this.log.silly(`Dequeued event UUID: ${evt.uuid}`);
|
|
625
801
|
await this.handleEvent(evt);
|
|
@@ -628,25 +804,26 @@ class Loxone extends utils.Adapter {
|
|
|
628
804
|
this.log.silly('Done with event queue');
|
|
629
805
|
}
|
|
630
806
|
}
|
|
631
|
-
|
|
632
|
-
|
|
807
|
+
|
|
808
|
+
private async handleEvent(evt: LoxoneEvent): Promise<void> {
|
|
633
809
|
const stateEventHandlerList = this.stateEventHandlers[evt.uuid];
|
|
634
810
|
if (stateEventHandlerList === undefined) {
|
|
635
811
|
this.log.debug(`Unknown event ${evt.uuid}: ${JSON.stringify(evt.evt)}`);
|
|
636
812
|
this.incInfoState('info.unknownEvents', evt.uuid, evt.evt);
|
|
637
813
|
return;
|
|
638
814
|
}
|
|
815
|
+
|
|
639
816
|
for (const item of stateEventHandlerList) {
|
|
640
817
|
try {
|
|
641
818
|
await item.handler(evt.evt);
|
|
642
|
-
}
|
|
643
|
-
catch (e) {
|
|
819
|
+
} catch (e: any) {
|
|
644
820
|
this.log.error(`Error while handling event UUID ${evt.uuid}: ${e}`);
|
|
645
|
-
|
|
821
|
+
this.getSentry()?.captureException(e, { extra: { evt } });
|
|
646
822
|
}
|
|
647
823
|
}
|
|
648
824
|
}
|
|
649
|
-
|
|
825
|
+
|
|
826
|
+
private async initInfoStates(): Promise<void> {
|
|
650
827
|
// Wait for states to load because if we don't, although the chances
|
|
651
828
|
// of processing starting before this actually completes is small, we
|
|
652
829
|
// should cater for that.
|
|
@@ -657,21 +834,25 @@ class Loxone extends utils.Adapter {
|
|
|
657
834
|
await this.initInfoState('info.stateChangesDiscarded');
|
|
658
835
|
await this.initInfoState('info.unknownEvents', true);
|
|
659
836
|
}
|
|
660
|
-
|
|
837
|
+
|
|
838
|
+
private async initInfoState(id: string, hasDetails = false): Promise<void> {
|
|
661
839
|
const state = await this.getStateAsync(id);
|
|
662
840
|
const initValue = state ? state.val : null;
|
|
663
|
-
const entry = {
|
|
841
|
+
const entry: InfoEntry = {
|
|
664
842
|
value: initValue,
|
|
665
843
|
lastSet: initValue,
|
|
666
|
-
timer:
|
|
844
|
+
timer: undefined,
|
|
667
845
|
};
|
|
846
|
+
|
|
668
847
|
if (hasDetails) {
|
|
669
848
|
// TODO: Maybe read these in so they persist across restarts?
|
|
670
|
-
entry.detailsMap = new Map();
|
|
849
|
+
entry.detailsMap = new Map<string, InfoDetailsEntry>();
|
|
671
850
|
}
|
|
851
|
+
|
|
672
852
|
this.info.set(id, entry);
|
|
673
853
|
}
|
|
674
|
-
|
|
854
|
+
|
|
855
|
+
private flushInfoStates(): void {
|
|
675
856
|
// Called on shutdown
|
|
676
857
|
this.info.forEach((infoEntry, key) => {
|
|
677
858
|
if (infoEntry.timer) {
|
|
@@ -681,15 +862,17 @@ class Loxone extends utils.Adapter {
|
|
|
681
862
|
}
|
|
682
863
|
});
|
|
683
864
|
}
|
|
684
|
-
|
|
865
|
+
|
|
866
|
+
private getInfoEntry(id: string): InfoEntry | undefined {
|
|
685
867
|
const infoEntry = this.info.get(id);
|
|
686
868
|
if (!infoEntry) {
|
|
687
869
|
// This should never happen!
|
|
688
|
-
this.log.error(
|
|
870
|
+
this.log.error(`No info entry for ${id}`);
|
|
689
871
|
}
|
|
690
872
|
return infoEntry;
|
|
691
873
|
}
|
|
692
|
-
|
|
874
|
+
|
|
875
|
+
private addInfoDetailsEntry(details: InfoDetailsEntryMap, id: string, value?: any): void {
|
|
693
876
|
/// ... and add details of this event to the map.
|
|
694
877
|
const eventEntry = details.get(id);
|
|
695
878
|
if (eventEntry) {
|
|
@@ -698,18 +881,17 @@ class Loxone extends utils.Adapter {
|
|
|
698
881
|
if (value !== undefined) {
|
|
699
882
|
eventEntry.lastValue = value;
|
|
700
883
|
}
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
884
|
+
} else {
|
|
703
885
|
// New entry
|
|
704
886
|
if (value !== undefined) {
|
|
705
887
|
details.set(id, { count: 1, lastValue: value });
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
888
|
+
} else {
|
|
708
889
|
details.set(id, { count: 1 });
|
|
709
890
|
}
|
|
710
891
|
}
|
|
711
892
|
}
|
|
712
|
-
|
|
893
|
+
|
|
894
|
+
private incInfoState(id: string, detailId?: string, detailValue?: any): void {
|
|
713
895
|
// Increment the given ID
|
|
714
896
|
const infoEntry = this.getInfoEntry(id);
|
|
715
897
|
if (infoEntry) {
|
|
@@ -724,59 +906,91 @@ class Loxone extends utils.Adapter {
|
|
|
724
906
|
}
|
|
725
907
|
}
|
|
726
908
|
}
|
|
727
|
-
|
|
909
|
+
|
|
910
|
+
private buildInfoDetails(src: InfoDetailsEntryMap): string {
|
|
728
911
|
// TODO: shouldn't this use JSON.stringify?
|
|
729
|
-
const out = [];
|
|
912
|
+
const out: any[] = [];
|
|
730
913
|
src.forEach((value, key) => {
|
|
731
914
|
if (value.lastValue !== undefined) {
|
|
732
915
|
out.push({ id: key, count: value.count, lastValue: value.lastValue });
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
916
|
+
} else {
|
|
735
917
|
out.push({ id: key, count: value.count });
|
|
736
918
|
}
|
|
737
919
|
});
|
|
738
920
|
return JSON.stringify(out);
|
|
739
921
|
}
|
|
740
|
-
|
|
922
|
+
|
|
923
|
+
private setInfoStateIfChanged(id: string, infoEntry: InfoEntry, shutdown = false): void {
|
|
741
924
|
if (infoEntry.value != infoEntry.lastSet) {
|
|
742
|
-
this.log.silly(
|
|
925
|
+
this.log.silly(`value of ${id} changed to ${infoEntry.value}`);
|
|
926
|
+
|
|
743
927
|
// Store counter
|
|
744
|
-
this.setState(id, infoEntry.value, true);
|
|
928
|
+
this.setState(id, infoEntry.value, true).catch(e => this.log.warn(e));
|
|
745
929
|
infoEntry.lastSet = infoEntry.value;
|
|
930
|
+
|
|
746
931
|
// Store any details
|
|
747
932
|
if (infoEntry.detailsMap) {
|
|
748
|
-
this.setState(id
|
|
933
|
+
this.setState(`${id}Detail`, this.buildInfoDetails(infoEntry.detailsMap), true).catch(e =>
|
|
934
|
+
this.log.warn(e),
|
|
935
|
+
);
|
|
749
936
|
}
|
|
937
|
+
|
|
750
938
|
if (!shutdown) {
|
|
751
939
|
// Start a timer which will set the current value from the info ID map on completion
|
|
752
940
|
// Obviously don't do this if called from shutdown
|
|
753
|
-
this.log.silly(
|
|
754
|
-
infoEntry.timer = this.setTimeout(
|
|
755
|
-
|
|
756
|
-
//
|
|
757
|
-
cbInfoEntry
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
941
|
+
this.log.silly(`Starting timer for ${id}`);
|
|
942
|
+
infoEntry.timer = this.setTimeout(
|
|
943
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
944
|
+
// @ts-ignore
|
|
945
|
+
(cbId: string, cbInfoEntry: InfoEntry) => {
|
|
946
|
+
this.log.silly(`Timeout for ${id}`);
|
|
947
|
+
|
|
948
|
+
// Remove from timer from map as we have just finished
|
|
949
|
+
cbInfoEntry.timer = undefined;
|
|
950
|
+
|
|
951
|
+
// Update the state, but only if the value in the info ID map has changed
|
|
952
|
+
this.setInfoStateIfChanged(cbId, cbInfoEntry);
|
|
953
|
+
},
|
|
954
|
+
30000, // Update every 30s max TODO: make this a config parameter?
|
|
955
|
+
id,
|
|
956
|
+
infoEntry, // Pass reference to entry
|
|
957
|
+
);
|
|
762
958
|
}
|
|
763
959
|
}
|
|
764
960
|
}
|
|
765
|
-
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Sends a command to the Loxone Miniserver.
|
|
964
|
+
*
|
|
965
|
+
* @param uuid The UUID of the command.
|
|
966
|
+
* @param action The name of the action to send.
|
|
967
|
+
*/
|
|
968
|
+
public sendCommand(uuid: string, action: string): void {
|
|
766
969
|
this.log.debug(`Sending command ${uuid} ${action}`);
|
|
767
970
|
this.incInfoState('info.messagesSent');
|
|
768
971
|
this.socket.send(`jdev/sps/io/${uuid}/${action}`, 2);
|
|
769
972
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Get an existing object by its ID.
|
|
976
|
+
*
|
|
977
|
+
* @param id The local ID of the object (without namespace).
|
|
978
|
+
* @returns The existing object or undefined if not found.
|
|
979
|
+
*/
|
|
980
|
+
public getExistingObject(id: string): ioBroker.Object | undefined {
|
|
981
|
+
const fullId = `${this.namespace}.${id}`;
|
|
982
|
+
return this.existingObjects[fullId];
|
|
776
983
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Update or create an object asynchronously.
|
|
987
|
+
*
|
|
988
|
+
* @param id The local ID of the object (without namespace).
|
|
989
|
+
* @param obj The object to update or create.
|
|
990
|
+
*/
|
|
991
|
+
public async updateObjectAsync(id: string, obj: ioBroker.SettableObject): Promise<void> {
|
|
992
|
+
const fullId = `${this.namespace}.${id}`;
|
|
993
|
+
if (this.existingObjects[fullId]) {
|
|
780
994
|
const existingObject = this.existingObjects[fullId];
|
|
781
995
|
if (!this.config.syncNames && obj.common) {
|
|
782
996
|
obj.common.name = existingObject.common.name;
|
|
@@ -787,9 +1001,24 @@ class Loxone extends utils.Adapter {
|
|
|
787
1001
|
obj.common.smartName = existingObject.common.smartName;
|
|
788
1002
|
}*/
|
|
789
1003
|
}
|
|
1004
|
+
|
|
790
1005
|
await this.extendObjectAsync(id, obj);
|
|
791
1006
|
}
|
|
792
|
-
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Update a state object asynchronously.
|
|
1010
|
+
*
|
|
1011
|
+
* @param id The local ID of the state (without namespace).
|
|
1012
|
+
* @param commonInfo The common information for the state.
|
|
1013
|
+
* @param stateUuid The UUID of the state.
|
|
1014
|
+
* @param stateEventHandler An optional event handler for state changes.
|
|
1015
|
+
*/
|
|
1016
|
+
public async updateStateObjectAsync(
|
|
1017
|
+
id: string,
|
|
1018
|
+
commonInfo: ioBroker.StateCommon,
|
|
1019
|
+
stateUuid: string,
|
|
1020
|
+
stateEventHandler?: NamedStateEventHandler,
|
|
1021
|
+
): Promise<void> {
|
|
793
1022
|
/* TODO: re-add:
|
|
794
1023
|
if (commonInfo.hasOwnProperty('smartIgnore')) {
|
|
795
1024
|
// interpret smartIgnore (our own extension of common) to generate smartName if needed
|
|
@@ -800,7 +1029,7 @@ class Loxone extends utils.Adapter {
|
|
|
800
1029
|
}
|
|
801
1030
|
delete commonInfo.smartIgnore;
|
|
802
1031
|
}*/
|
|
803
|
-
const obj = {
|
|
1032
|
+
const obj: ioBroker.SettableObjectWorker<ioBroker.StateObject> = {
|
|
804
1033
|
type: 'state',
|
|
805
1034
|
common: commonInfo,
|
|
806
1035
|
native: {
|
|
@@ -809,24 +1038,43 @@ class Loxone extends utils.Adapter {
|
|
|
809
1038
|
};
|
|
810
1039
|
await this.updateObjectAsync(id, obj);
|
|
811
1040
|
if (stateEventHandler) {
|
|
812
|
-
this.addStateEventHandler(stateUuid, async (value) => {
|
|
1041
|
+
this.addStateEventHandler(stateUuid, async (value: ioBroker.StateValue) => {
|
|
813
1042
|
await stateEventHandler(id, value);
|
|
814
1043
|
});
|
|
815
1044
|
}
|
|
816
1045
|
}
|
|
817
|
-
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Add a state event handler for a given UUID.
|
|
1049
|
+
*
|
|
1050
|
+
* @param uuid The UUID of the control.
|
|
1051
|
+
* @param eventHandler The event handler function.
|
|
1052
|
+
* @param name An optional name for the event handler.
|
|
1053
|
+
*/
|
|
1054
|
+
public addStateEventHandler(uuid: string, eventHandler: StateEventHandler, name?: string): void {
|
|
818
1055
|
if (this.stateEventHandlers[uuid] === undefined) {
|
|
819
1056
|
this.stateEventHandlers[uuid] = [];
|
|
820
1057
|
}
|
|
1058
|
+
|
|
821
1059
|
if (name) {
|
|
822
1060
|
this.removeStateEventHandler(uuid, name);
|
|
823
1061
|
}
|
|
1062
|
+
|
|
824
1063
|
this.stateEventHandlers[uuid].push({ name: name, handler: eventHandler });
|
|
825
1064
|
}
|
|
826
|
-
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Remove a state event handler for a given UUID by name.
|
|
1068
|
+
*
|
|
1069
|
+
* @param uuid The UUID of the control.
|
|
1070
|
+
* @param name The name of the event handler to remove.
|
|
1071
|
+
* @returns True if the handler was found and removed, false otherwise.
|
|
1072
|
+
*/
|
|
1073
|
+
public removeStateEventHandler(uuid: string, name: string): boolean {
|
|
827
1074
|
if (this.stateEventHandlers[uuid] === undefined || !name) {
|
|
828
1075
|
return false;
|
|
829
1076
|
}
|
|
1077
|
+
|
|
830
1078
|
let found = false;
|
|
831
1079
|
for (let i = 0; i < this.stateEventHandlers[uuid].length; i++) {
|
|
832
1080
|
if (this.stateEventHandlers[uuid][i].name === name) {
|
|
@@ -834,17 +1082,27 @@ class Loxone extends utils.Adapter {
|
|
|
834
1082
|
found = true;
|
|
835
1083
|
}
|
|
836
1084
|
}
|
|
1085
|
+
|
|
837
1086
|
return found;
|
|
838
1087
|
}
|
|
839
|
-
|
|
840
|
-
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Add a state change listener for a given state ID.
|
|
1091
|
+
*
|
|
1092
|
+
* @param id The local ID of the state (without namespace).
|
|
1093
|
+
* @param listener The state change listener function.
|
|
1094
|
+
* @param opts Optional options for the state change listener.
|
|
1095
|
+
*/
|
|
1096
|
+
public addStateChangeListener(id: string, listener: StateChangeListener, opts?: StateChangeListenerOpts): void {
|
|
1097
|
+
this.stateChangeListeners[`${this.namespace}.${id}`] = {
|
|
841
1098
|
listener,
|
|
842
1099
|
opts,
|
|
843
1100
|
queuedVal: null,
|
|
844
|
-
ackTimer:
|
|
1101
|
+
ackTimer: undefined,
|
|
845
1102
|
};
|
|
846
1103
|
}
|
|
847
|
-
|
|
1104
|
+
|
|
1105
|
+
private async checkStateForAck(id: string): Promise<void> {
|
|
848
1106
|
const stateChangeListener = this.stateChangeListeners[id];
|
|
849
1107
|
if (stateChangeListener) {
|
|
850
1108
|
// This state change could be a result of a command we sent being ack'd
|
|
@@ -852,32 +1110,47 @@ class Loxone extends utils.Adapter {
|
|
|
852
1110
|
// Timer is running so clear it
|
|
853
1111
|
this.log.debug(`Clearing ackTimer for ${id}`);
|
|
854
1112
|
this.clearTimeout(stateChangeListener.ackTimer);
|
|
855
|
-
stateChangeListener.ackTimer =
|
|
1113
|
+
stateChangeListener.ackTimer = undefined;
|
|
856
1114
|
// Send any command that may have been delayed waiting for this ack
|
|
857
1115
|
await this.handleDelayedStateChange(id, stateChangeListener);
|
|
858
|
-
}
|
|
859
|
-
else {
|
|
1116
|
+
} else {
|
|
860
1117
|
this.log.debug(`No ackTimer for ${id}`);
|
|
861
1118
|
}
|
|
862
|
-
}
|
|
863
|
-
else {
|
|
1119
|
+
} else {
|
|
864
1120
|
this.log.silly(`${id} has no stateChangeListener`);
|
|
865
1121
|
}
|
|
866
1122
|
}
|
|
867
|
-
|
|
868
|
-
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Set state with ack = true and update cached value.
|
|
1126
|
+
*
|
|
1127
|
+
* @param id The local ID of the state (without namespace).
|
|
1128
|
+
* @param value The value to set.
|
|
1129
|
+
*/
|
|
1130
|
+
public async setStateAck(id: string, value: CurrentStateValue): Promise<void> {
|
|
1131
|
+
const keyId = `${this.namespace}.${id}`;
|
|
869
1132
|
this.currentStateValues[keyId] = value;
|
|
870
1133
|
await this.checkStateForAck(keyId);
|
|
871
1134
|
await this.setStateAsync(id, { val: value, ack: true });
|
|
872
1135
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Get a cached state value.
|
|
1139
|
+
*
|
|
1140
|
+
* @param id The local ID of the state (without namespace).
|
|
1141
|
+
* @returns The cached state value.
|
|
1142
|
+
*/
|
|
1143
|
+
public getCachedStateValue(id: string): OldStateValue {
|
|
1144
|
+
const keyId = `${this.namespace}.${id}`;
|
|
1145
|
+
return this.currentStateValues[keyId];
|
|
879
1146
|
}
|
|
880
|
-
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Get the Sentry instance if available.
|
|
1150
|
+
*
|
|
1151
|
+
* @returns The Sentry instance or undefined if not available.
|
|
1152
|
+
*/
|
|
1153
|
+
public getSentry(): Sentry | undefined {
|
|
881
1154
|
if (this.supportsFeature && this.supportsFeature('PLUGINS')) {
|
|
882
1155
|
const sentryInstance = this.getPluginInstance('sentry');
|
|
883
1156
|
if (sentryInstance) {
|
|
@@ -885,19 +1158,21 @@ class Loxone extends utils.Adapter {
|
|
|
885
1158
|
}
|
|
886
1159
|
}
|
|
887
1160
|
}
|
|
888
|
-
|
|
889
|
-
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Report an error message to Sentry.
|
|
1164
|
+
*
|
|
1165
|
+
* @param message The error message to report.
|
|
1166
|
+
*/
|
|
1167
|
+
public reportError(message: string): void {
|
|
890
1168
|
this.log.error(message);
|
|
891
|
-
|
|
1169
|
+
this.getSentry()?.captureMessage(message, 'error');
|
|
892
1170
|
}
|
|
893
1171
|
}
|
|
894
|
-
|
|
895
|
-
if (module.parent) {
|
|
1172
|
+
if (require.main !== module) {
|
|
896
1173
|
// Export the constructor in compact mode
|
|
897
|
-
module.exports = (options) => new Loxone(options);
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
1174
|
+
module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Loxone(options);
|
|
1175
|
+
} else {
|
|
900
1176
|
// otherwise start the instance directly
|
|
901
1177
|
(() => new Loxone())();
|
|
902
1178
|
}
|
|
903
|
-
//# sourceMappingURL=main.js.map
|