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