iobroker.flowers 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -75,6 +75,11 @@ Only one watering cycle runs at a time per plant. Configure the duration in Sett
75
75
 
76
76
  ## Changelog
77
77
 
78
+ ### 0.3.3 (2026-03-30)
79
+ - (sadam6752-tech) Fix object hierarchy: create device/channel parent objects before states
80
+ - (sadam6752-tech) Use correct state roles: value.humidity, value.temperature, value.battery
81
+ - (sadam6752-tech) Improve unload: null timers after clearing, guard monitor null check
82
+
78
83
  ### 0.3.2 (2026-03-30)
79
84
  - (sadam6752-tech) Custom profiles: users can create own plant profiles in Profiles tab
80
85
  - (sadam6752-tech) Custom profile field in Plants table for direct profile name entry
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "flowers",
4
- "version": "0.3.2",
4
+ "version": "0.3.3",
5
5
  "news": {
6
+ "0.3.3": {
7
+ "en": "Fix object hierarchy (device/channel/state), correct state roles (value.humidity, value.temperature, value.battery), improve unload cleanup",
8
+ "de": "Objekthierarchie korrigiert (device/channel/state), korrekte State-Rollen, verbessertes Aufräumen beim Entladen",
9
+ "ru": "Исправлена иерархия объектов (device/channel/state), правильные роли состояний, улучшена очистка при выгрузке",
10
+ "fr": "Correction hiérarchie objets (device/channel/state), rôles d'état corrects, nettoyage amélioré au déchargement",
11
+ "it": "Corretta gerarchia oggetti (device/channel/state), ruoli stato corretti, pulizia migliorata allo scaricamento",
12
+ "es": "Corrección jerarquía objetos (device/channel/state), roles de estado correctos, limpieza mejorada al descargar",
13
+ "pl": "Poprawiono hierarchię obiektów (device/channel/state), poprawne role stanów, ulepszone czyszczenie przy wyładowaniu",
14
+ "pt": "Correção hierarquia objetos (device/channel/state), funções de estado corretas, limpeza melhorada no descarregamento",
15
+ "nl": "Objecthiërarchie gecorrigeerd (device/channel/state), correcte state-rollen, verbeterde opruiming bij ontladen",
16
+ "uk": "Виправлено ієрархію об'єктів (device/channel/state), правильні ролі станів, покращено очищення при вивантаженні",
17
+ "zh-cn": "修复对象层次结构(device/channel/state),正确的状态角色,改进卸载清理"
18
+ },
6
19
  "0.3.2": {
7
20
  "en": "Custom profiles: users can create own plant profiles in Profiles tab",
8
21
  "de": "Benutzerdefinierte Profile: eigene Pflanzenprofile in der Profiles-Registerkarte erstellen",
@@ -1,383 +1,409 @@
1
- "use strict";
2
-
3
- const profiles = require("./plant-profiles.json");
4
- const { msg } = require("./messages");
5
-
6
- /**
7
- * MonitorService — подписка на датчики, проверка порогов, генерация алертов.
8
- */
9
- class MonitorService {
10
- /**
11
- * @param {object} adapter - ioBroker adapter instance
12
- * @param {object} notificationManager - NotificationManager instance
13
- * @param {string} lang - system language code
14
- */
15
- constructor(adapter, notificationManager, lang) {
16
- this.adapter = adapter;
17
- this.notif = notificationManager;
18
- this.lang = lang || "en";
19
- this.sensorValues = {};
20
- this.lastSeen = {};
21
- // active watering timers: key = plant.name, value = true if watering in progress
22
- this._wateringActive = {};
23
- }
24
-
25
- /**
26
- * Get effective thresholds for a plant (plant overrides profile).
27
- *
28
- * @param {object} plant - plant configuration object
29
- * @returns {object} effective threshold values
30
- */
31
- _getThresholds(plant) {
32
- // First check custom profiles from config, then built-in profiles
33
- const customProfiles = this.adapter.config.customProfiles || [];
34
- const profileName = plant.customProfile || plant.profile;
35
- const customProfile = customProfiles.find((p) => p.name === profileName);
36
- const profile =
37
- customProfile || profiles[profileName] || profiles["Custom"];
38
- const override = (val, fallback) => {
39
- if (val === null || val === undefined || val === "") {
40
- return fallback;
41
- }
42
- const n = parseFloat(val);
43
- return isNaN(n) ? fallback : n;
44
- };
45
- return {
46
- humidityMin: override(plant.humidityMin, profile.humidityMin),
47
- humidityMax: override(plant.humidityMax, profile.humidityMax),
48
- temperatureMin: override(plant.temperatureMin, profile.temperatureMin),
49
- temperatureMax: override(plant.temperatureMax, profile.temperatureMax),
50
- batteryMin: override(plant.batteryMin, profile.batteryMin),
51
- };
52
- }
53
-
54
- /**
55
- * Subscribe to all sensor states for all enabled plants.
56
- */
57
- async subscribeAll() {
58
- const plants = this.adapter.config.plants || [];
59
- for (const plant of plants) {
60
- if (!plant.enabled) {
61
- continue;
62
- }
63
- for (const attr of [
64
- "sensorHumidity",
65
- "sensorTemperature",
66
- "sensorBattery",
67
- ]) {
68
- const stateId = plant[attr];
69
- if (stateId) {
70
- await this.adapter.subscribeForeignStatesAsync(stateId);
71
- this.adapter.log.debug(`MonitorService: subscribed to ${stateId}`);
72
- // Read initial value so checkAll() works immediately
73
- try {
74
- const state = await this.adapter.getForeignStateAsync(stateId);
75
- if (state && state.val !== null && state.val !== undefined) {
76
- this.sensorValues[stateId] = {
77
- val: state.val,
78
- ts: state.ts || Date.now(),
79
- };
80
- this.lastSeen[plant.name] = Date.now();
81
- this.adapter.log.debug(
82
- `MonitorService: initial value ${stateId} = ${state.val}`,
83
- );
84
- }
85
- } catch (err) {
86
- this.adapter.log.warn(
87
- `MonitorService: could not read initial value for ${stateId}: ${err.message}`,
88
- );
89
- }
90
- }
91
- }
92
- }
93
- }
94
-
95
- /**
96
- * Unsubscribe from all sensor states.
97
- */
98
- async unsubscribeAll() {
99
- const plants = this.adapter.config.plants || [];
100
- for (const plant of plants) {
101
- for (const attr of [
102
- "sensorHumidity",
103
- "sensorTemperature",
104
- "sensorBattery",
105
- ]) {
106
- const stateId = plant[attr];
107
- if (stateId) {
108
- await this.adapter.unsubscribeForeignStatesAsync(stateId);
109
- }
110
- }
111
- }
112
- }
113
-
114
- /**
115
- * Handle incoming state change from subscribed sensor.
116
- *
117
- * @param {string} id - state id
118
- * @param {object} state - ioBroker state object
119
- */
120
- async onStateChange(id, state) {
121
- if (!state || state.val === null || state.val === undefined) {
122
- return;
123
- }
124
-
125
- // Ensure numeric value (sensors may send strings)
126
- const numVal = parseFloat(state.val);
127
- if (isNaN(numVal)) {
128
- return;
129
- }
130
- this.sensorValues[id] = { val: numVal, ts: state.ts || Date.now() };
131
-
132
- const plants = this.adapter.config.plants || [];
133
- for (const plant of plants) {
134
- if (!plant.enabled) {
135
- continue;
136
- }
137
- if (
138
- plant.sensorHumidity === id ||
139
- plant.sensorTemperature === id ||
140
- plant.sensorBattery === id
141
- ) {
142
- this.lastSeen[plant.name] = Date.now();
143
- await this._checkPlant(plant);
144
- }
145
- }
146
- }
147
-
148
- /**
149
- * Check all plants against current sensor values (called on interval).
150
- */
151
- async checkAll() {
152
- const plants = this.adapter.config.plants || [];
153
- for (const plant of plants) {
154
- if (!plant.enabled) {
155
- continue;
156
- }
157
- await this._checkPlant(plant);
158
- await this._checkOffline(plant);
159
- }
160
- }
161
-
162
- /**
163
- * Check a single plant's thresholds and send alerts if needed.
164
- *
165
- * @param {object} plant - plant configuration object
166
- */
167
- async _checkPlant(plant) {
168
- const thresholds = this._getThresholds(plant);
169
- const label = `${plant.name}${plant.location ? ` (${plant.location})` : ""}`;
170
- this.adapter.log.debug(
171
- `MonitorService: checking plant "${plant.name}", thresholds: hum ${thresholds.humidityMin}-${thresholds.humidityMax}%`,
172
- );
173
-
174
- // Humidity
175
- if (plant.sensorHumidity) {
176
- const entry = this.sensorValues[plant.sensorHumidity];
177
- if (entry != null) {
178
- const val = entry.val;
179
- this.adapter.log.debug(
180
- `MonitorService: ${plant.name} humidity=${val}% (min=${thresholds.humidityMin}, max=${thresholds.humidityMax})`,
181
- );
182
- if (val < thresholds.humidityMin) {
183
- await this.notif.send(
184
- msg("humidity_low", this.lang, label, val, thresholds.humidityMin),
185
- `${plant.name}:humidity_low`,
186
- );
187
- await this._startWatering(plant);
188
- } else if (val > thresholds.humidityMax) {
189
- await this.notif.send(
190
- msg("humidity_high", this.lang, label, val, thresholds.humidityMax),
191
- `${plant.name}:humidity_high`,
192
- );
193
- }
194
- await this._updateState(
195
- `plants.${this._safeName(plant.name)}.humidity`,
196
- val,
197
- "%",
198
- );
199
- }
200
- }
201
-
202
- // Temperature
203
- if (plant.sensorTemperature) {
204
- const entry = this.sensorValues[plant.sensorTemperature];
205
- if (entry != null) {
206
- const val = entry.val;
207
- if (val < thresholds.temperatureMin) {
208
- await this.notif.send(
209
- msg("temp_low", this.lang, label, val, thresholds.temperatureMin),
210
- `${plant.name}:temp_low`,
211
- );
212
- } else if (val > thresholds.temperatureMax) {
213
- await this.notif.send(
214
- msg("temp_high", this.lang, label, val, thresholds.temperatureMax),
215
- `${plant.name}:temp_high`,
216
- );
217
- }
218
- await this._updateState(
219
- `plants.${this._safeName(plant.name)}.temperature`,
220
- val,
221
- "°C",
222
- );
223
- }
224
- }
225
-
226
- // Battery
227
- if (plant.sensorBattery) {
228
- const entry = this.sensorValues[plant.sensorBattery];
229
- if (entry != null) {
230
- const val = entry.val;
231
- if (val < thresholds.batteryMin) {
232
- await this.notif.send(
233
- msg("battery_low", this.lang, label, val, thresholds.batteryMin),
234
- `${plant.name}:battery_low`,
235
- );
236
- }
237
- await this._updateState(
238
- `plants.${this._safeName(plant.name)}.battery`,
239
- val,
240
- "%",
241
- );
242
- }
243
- }
244
- }
245
-
246
- /**
247
- * Check if a plant sensor has gone offline.
248
- *
249
- * @param {object} plant - plant configuration object
250
- */
251
- async _checkOffline(plant) {
252
- const offlineHours = parseInt(this.adapter.config.offlineThreshold) || 3;
253
- const last = this.lastSeen[plant.name];
254
- if (!last) {
255
- return;
256
- }
257
- const diffHours = (Date.now() - last) / (1000 * 60 * 60);
258
- if (diffHours >= offlineHours) {
259
- const label = `${plant.name}${plant.location ? ` (${plant.location})` : ""}`;
260
- await this.notif.send(
261
- msg("offline", this.lang, label, Math.round(diffHours)),
262
- `${plant.name}:offline`,
263
- );
264
- }
265
- }
266
-
267
- /**
268
- * Start automatic watering for a plant if configured and not already active.
269
- *
270
- * @param {object} plant - plant configuration object
271
- */
272
- async _startWatering(plant) {
273
- if (!plant.sensorWatering) {
274
- return;
275
- }
276
- if (this._wateringActive[plant.name]) {
277
- this.adapter.log.debug(
278
- `MonitorService: watering already active for "${plant.name}", skipping`,
279
- );
280
- return;
281
- }
282
-
283
- const durationMin = parseInt(this.adapter.config.wateringDuration) || 3;
284
- const durationMs = durationMin * 60 * 1000;
285
-
286
- try {
287
- await this.adapter.setForeignStateAsync(plant.sensorWatering, {
288
- val: true,
289
- ack: false,
290
- });
291
- this._wateringActive[plant.name] = true;
292
- this.adapter.log.info(
293
- `MonitorService: watering started for "${plant.name}" (${durationMin} min)`,
294
- );
295
-
296
- this.adapter.setTimeout(async () => {
297
- try {
298
- await this.adapter.setForeignStateAsync(plant.sensorWatering, {
299
- val: false,
300
- ack: false,
301
- });
302
- this.adapter.log.info(
303
- `MonitorService: watering stopped for "${plant.name}"`,
304
- );
305
- } catch (err) {
306
- this.adapter.log.warn(
307
- `MonitorService: failed to stop watering for "${plant.name}": ${err.message}`,
308
- );
309
- }
310
- this._wateringActive[plant.name] = false;
311
- }, durationMs);
312
- } catch (err) {
313
- this.adapter.log.warn(
314
- `MonitorService: failed to start watering for "${plant.name}": ${err.message}`,
315
- );
316
- this._wateringActive[plant.name] = false;
317
- }
318
- }
319
-
320
- /**
321
- * Update or create a state for a plant sensor value.
322
- *
323
- * @param {string} id - relative state id
324
- * @param {number} val - sensor value
325
- * @param {string} unit - unit of measurement
326
- */
327
- async _updateState(id, val, unit) {
328
- try {
329
- await this.adapter.extendObjectAsync(id, {
330
- type: "state",
331
- common: {
332
- name: id,
333
- type: "number",
334
- role: "value",
335
- unit,
336
- read: true,
337
- write: false,
338
- },
339
- native: {},
340
- });
341
- await this.adapter.setStateAsync(id, { val, ack: true });
342
- } catch (err) {
343
- this.adapter.log.warn(
344
- `MonitorService: failed to update state ${id}: ${err.message}`,
345
- );
346
- }
347
- }
348
-
349
- /**
350
- * Convert plant name to safe state id segment.
351
- *
352
- * @param {string} name - plant name
353
- * @returns {string} sanitized name safe for use as state id
354
- */
355
- _safeName(name) {
356
- return name.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
357
- }
358
-
359
- /**
360
- * Get current sensor values for all enabled plants (for reports).
361
- *
362
- * @returns {Array<object>} array of { plant, humidity, temperature, battery }
363
- */
364
- getPlantStates() {
365
- const plants = this.adapter.config.plants || [];
366
- return plants
367
- .filter((p) => p.enabled)
368
- .map((plant) => ({
369
- plant,
370
- humidity: plant.sensorHumidity
371
- ? (this.sensorValues[plant.sensorHumidity]?.val ?? null)
372
- : null,
373
- temperature: plant.sensorTemperature
374
- ? (this.sensorValues[plant.sensorTemperature]?.val ?? null)
375
- : null,
376
- battery: plant.sensorBattery
377
- ? (this.sensorValues[plant.sensorBattery]?.val ?? null)
378
- : null,
379
- }));
380
- }
381
- }
382
-
383
- module.exports = MonitorService;
1
+ "use strict";
2
+
3
+ const profiles = require("./plant-profiles.json");
4
+ const { msg } = require("./messages");
5
+
6
+ /**
7
+ * MonitorService — подписка на датчики, проверка порогов, генерация алертов.
8
+ */
9
+ class MonitorService {
10
+ /**
11
+ * @param {object} adapter - ioBroker adapter instance
12
+ * @param {object} notificationManager - NotificationManager instance
13
+ * @param {string} lang - system language code
14
+ */
15
+ constructor(adapter, notificationManager, lang) {
16
+ this.adapter = adapter;
17
+ this.notif = notificationManager;
18
+ this.lang = lang || "en";
19
+ this.sensorValues = {};
20
+ this.lastSeen = {};
21
+ // active watering timers: key = plant.name, value = true if watering in progress
22
+ this._wateringActive = {};
23
+ }
24
+
25
+ /**
26
+ * Get effective thresholds for a plant (plant overrides profile).
27
+ *
28
+ * @param {object} plant - plant configuration object
29
+ * @returns {object} effective threshold values
30
+ */
31
+ _getThresholds(plant) {
32
+ // First check custom profiles from config, then built-in profiles
33
+ const customProfiles = this.adapter.config.customProfiles || [];
34
+ const profileName = plant.customProfile || plant.profile;
35
+ const customProfile = customProfiles.find((p) => p.name === profileName);
36
+ const profile =
37
+ customProfile || profiles[profileName] || profiles["Custom"];
38
+ const override = (val, fallback) => {
39
+ if (val === null || val === undefined || val === "") {
40
+ return fallback;
41
+ }
42
+ const n = parseFloat(val);
43
+ return isNaN(n) ? fallback : n;
44
+ };
45
+ return {
46
+ humidityMin: override(plant.humidityMin, profile.humidityMin),
47
+ humidityMax: override(plant.humidityMax, profile.humidityMax),
48
+ temperatureMin: override(plant.temperatureMin, profile.temperatureMin),
49
+ temperatureMax: override(plant.temperatureMax, profile.temperatureMax),
50
+ batteryMin: override(plant.batteryMin, profile.batteryMin),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Subscribe to all sensor states for all enabled plants.
56
+ */
57
+ async subscribeAll() {
58
+ const plants = this.adapter.config.plants || [];
59
+ for (const plant of plants) {
60
+ if (!plant.enabled) {
61
+ continue;
62
+ }
63
+ for (const attr of [
64
+ "sensorHumidity",
65
+ "sensorTemperature",
66
+ "sensorBattery",
67
+ ]) {
68
+ const stateId = plant[attr];
69
+ if (stateId) {
70
+ await this.adapter.subscribeForeignStatesAsync(stateId);
71
+ this.adapter.log.debug(`MonitorService: subscribed to ${stateId}`);
72
+ // Read initial value so checkAll() works immediately
73
+ try {
74
+ const state = await this.adapter.getForeignStateAsync(stateId);
75
+ if (state && state.val !== null && state.val !== undefined) {
76
+ this.sensorValues[stateId] = {
77
+ val: state.val,
78
+ ts: state.ts || Date.now(),
79
+ };
80
+ this.lastSeen[plant.name] = Date.now();
81
+ this.adapter.log.debug(
82
+ `MonitorService: initial value ${stateId} = ${state.val}`,
83
+ );
84
+ }
85
+ } catch (err) {
86
+ this.adapter.log.warn(
87
+ `MonitorService: could not read initial value for ${stateId}: ${err.message}`,
88
+ );
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Unsubscribe from all sensor states.
97
+ */
98
+ async unsubscribeAll() {
99
+ const plants = this.adapter.config.plants || [];
100
+ for (const plant of plants) {
101
+ for (const attr of [
102
+ "sensorHumidity",
103
+ "sensorTemperature",
104
+ "sensorBattery",
105
+ ]) {
106
+ const stateId = plant[attr];
107
+ if (stateId) {
108
+ await this.adapter.unsubscribeForeignStatesAsync(stateId);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Handle incoming state change from subscribed sensor.
116
+ *
117
+ * @param {string} id - state id
118
+ * @param {object} state - ioBroker state object
119
+ */
120
+ async onStateChange(id, state) {
121
+ if (!state || state.val === null || state.val === undefined) {
122
+ return;
123
+ }
124
+
125
+ // Ensure numeric value (sensors may send strings)
126
+ const numVal = parseFloat(state.val);
127
+ if (isNaN(numVal)) {
128
+ return;
129
+ }
130
+ this.sensorValues[id] = { val: numVal, ts: state.ts || Date.now() };
131
+
132
+ const plants = this.adapter.config.plants || [];
133
+ for (const plant of plants) {
134
+ if (!plant.enabled) {
135
+ continue;
136
+ }
137
+ if (
138
+ plant.sensorHumidity === id ||
139
+ plant.sensorTemperature === id ||
140
+ plant.sensorBattery === id
141
+ ) {
142
+ this.lastSeen[plant.name] = Date.now();
143
+ await this._checkPlant(plant);
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Check all plants against current sensor values (called on interval).
150
+ */
151
+ async checkAll() {
152
+ const plants = this.adapter.config.plants || [];
153
+ for (const plant of plants) {
154
+ if (!plant.enabled) {
155
+ continue;
156
+ }
157
+ await this._checkPlant(plant);
158
+ await this._checkOffline(plant);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Check a single plant's thresholds and send alerts if needed.
164
+ *
165
+ * @param {object} plant - plant configuration object
166
+ */
167
+ async _checkPlant(plant) {
168
+ const thresholds = this._getThresholds(plant);
169
+ const label = `${plant.name}${plant.location ? ` (${plant.location})` : ""}`;
170
+ this.adapter.log.debug(
171
+ `MonitorService: checking plant "${plant.name}", thresholds: hum ${thresholds.humidityMin}-${thresholds.humidityMax}%`,
172
+ );
173
+
174
+ // Humidity
175
+ if (plant.sensorHumidity) {
176
+ const entry = this.sensorValues[plant.sensorHumidity];
177
+ if (entry != null) {
178
+ const val = entry.val;
179
+ this.adapter.log.debug(
180
+ `MonitorService: ${plant.name} humidity=${val}% (min=${thresholds.humidityMin}, max=${thresholds.humidityMax})`,
181
+ );
182
+ if (val < thresholds.humidityMin) {
183
+ await this.notif.send(
184
+ msg("humidity_low", this.lang, label, val, thresholds.humidityMin),
185
+ `${plant.name}:humidity_low`,
186
+ );
187
+ await this._startWatering(plant);
188
+ } else if (val > thresholds.humidityMax) {
189
+ await this.notif.send(
190
+ msg("humidity_high", this.lang, label, val, thresholds.humidityMax),
191
+ `${plant.name}:humidity_high`,
192
+ );
193
+ }
194
+ await this._updateState(
195
+ `plants.${this._safeName(plant.name)}.humidity`,
196
+ val,
197
+ "%",
198
+ );
199
+ }
200
+ }
201
+
202
+ // Temperature
203
+ if (plant.sensorTemperature) {
204
+ const entry = this.sensorValues[plant.sensorTemperature];
205
+ if (entry != null) {
206
+ const val = entry.val;
207
+ if (val < thresholds.temperatureMin) {
208
+ await this.notif.send(
209
+ msg("temp_low", this.lang, label, val, thresholds.temperatureMin),
210
+ `${plant.name}:temp_low`,
211
+ );
212
+ } else if (val > thresholds.temperatureMax) {
213
+ await this.notif.send(
214
+ msg("temp_high", this.lang, label, val, thresholds.temperatureMax),
215
+ `${plant.name}:temp_high`,
216
+ );
217
+ }
218
+ await this._updateState(
219
+ `plants.${this._safeName(plant.name)}.temperature`,
220
+ val,
221
+ "°C",
222
+ );
223
+ }
224
+ }
225
+
226
+ // Battery
227
+ if (plant.sensorBattery) {
228
+ const entry = this.sensorValues[plant.sensorBattery];
229
+ if (entry != null) {
230
+ const val = entry.val;
231
+ if (val < thresholds.batteryMin) {
232
+ await this.notif.send(
233
+ msg("battery_low", this.lang, label, val, thresholds.batteryMin),
234
+ `${plant.name}:battery_low`,
235
+ );
236
+ }
237
+ await this._updateState(
238
+ `plants.${this._safeName(plant.name)}.battery`,
239
+ val,
240
+ "%",
241
+ );
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Check if a plant sensor has gone offline.
248
+ *
249
+ * @param {object} plant - plant configuration object
250
+ */
251
+ async _checkOffline(plant) {
252
+ const offlineHours = parseInt(this.adapter.config.offlineThreshold) || 3;
253
+ const last = this.lastSeen[plant.name];
254
+ if (!last) {
255
+ return;
256
+ }
257
+ const diffHours = (Date.now() - last) / (1000 * 60 * 60);
258
+ if (diffHours >= offlineHours) {
259
+ const label = `${plant.name}${plant.location ? ` (${plant.location})` : ""}`;
260
+ await this.notif.send(
261
+ msg("offline", this.lang, label, Math.round(diffHours)),
262
+ `${plant.name}:offline`,
263
+ );
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Start automatic watering for a plant if configured and not already active.
269
+ *
270
+ * @param {object} plant - plant configuration object
271
+ */
272
+ async _startWatering(plant) {
273
+ if (!plant.sensorWatering) {
274
+ return;
275
+ }
276
+ if (this._wateringActive[plant.name]) {
277
+ this.adapter.log.debug(
278
+ `MonitorService: watering already active for "${plant.name}", skipping`,
279
+ );
280
+ return;
281
+ }
282
+
283
+ const durationMin = parseInt(this.adapter.config.wateringDuration) || 3;
284
+ const durationMs = durationMin * 60 * 1000;
285
+
286
+ try {
287
+ await this.adapter.setForeignStateAsync(plant.sensorWatering, {
288
+ val: true,
289
+ ack: false,
290
+ });
291
+ this._wateringActive[plant.name] = true;
292
+ this.adapter.log.info(
293
+ `MonitorService: watering started for "${plant.name}" (${durationMin} min)`,
294
+ );
295
+
296
+ this.adapter.setTimeout(async () => {
297
+ try {
298
+ await this.adapter.setForeignStateAsync(plant.sensorWatering, {
299
+ val: false,
300
+ ack: false,
301
+ });
302
+ this.adapter.log.info(
303
+ `MonitorService: watering stopped for "${plant.name}"`,
304
+ );
305
+ } catch (err) {
306
+ this.adapter.log.warn(
307
+ `MonitorService: failed to stop watering for "${plant.name}": ${err.message}`,
308
+ );
309
+ }
310
+ this._wateringActive[plant.name] = false;
311
+ }, durationMs);
312
+ } catch (err) {
313
+ this.adapter.log.warn(
314
+ `MonitorService: failed to start watering for "${plant.name}": ${err.message}`,
315
+ );
316
+ this._wateringActive[plant.name] = false;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Update or create a state for a plant sensor value.
322
+ * Ensures parent device and channel objects exist first.
323
+ *
324
+ * @param {string} id - relative state id (e.g. plants.ficus.humidity)
325
+ * @param {number} val - sensor value
326
+ * @param {string} unit - unit of measurement
327
+ */
328
+ async _updateState(id, val, unit) {
329
+ try {
330
+ // Ensure parent objects exist: plants (channel) → plants.<name> (device)
331
+ const parts = id.split(".");
332
+ if (parts.length === 3) {
333
+ // parts = ['plants', '<safeName>', '<sensor>']
334
+ await this.adapter.extendObjectAsync(parts[0], {
335
+ type: "channel",
336
+ common: { name: "Plants" },
337
+ native: {},
338
+ });
339
+ await this.adapter.extendObjectAsync(`${parts[0]}.${parts[1]}`, {
340
+ type: "device",
341
+ common: { name: parts[1] },
342
+ native: {},
343
+ });
344
+ }
345
+
346
+ // Determine correct role
347
+ const roleMap = {
348
+ humidity: "value.humidity",
349
+ temperature: "value.temperature",
350
+ battery: "value.battery",
351
+ };
352
+ const sensor = parts[parts.length - 1];
353
+ const role = roleMap[sensor] || "value";
354
+
355
+ await this.adapter.extendObjectAsync(id, {
356
+ type: "state",
357
+ common: {
358
+ name: sensor,
359
+ type: "number",
360
+ role,
361
+ unit,
362
+ read: true,
363
+ write: false,
364
+ },
365
+ native: {},
366
+ });
367
+ await this.adapter.setStateAsync(id, { val, ack: true });
368
+ } catch (err) {
369
+ this.adapter.log.warn(
370
+ `MonitorService: failed to update state ${id}: ${err.message}`,
371
+ );
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Convert plant name to safe state id segment.
377
+ *
378
+ * @param {string} name - plant name
379
+ * @returns {string} sanitized name safe for use as state id
380
+ */
381
+ _safeName(name) {
382
+ return name.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
383
+ }
384
+
385
+ /**
386
+ * Get current sensor values for all enabled plants (for reports).
387
+ *
388
+ * @returns {Array<object>} array of { plant, humidity, temperature, battery }
389
+ */
390
+ getPlantStates() {
391
+ const plants = this.adapter.config.plants || [];
392
+ return plants
393
+ .filter((p) => p.enabled)
394
+ .map((plant) => ({
395
+ plant,
396
+ humidity: plant.sensorHumidity
397
+ ? (this.sensorValues[plant.sensorHumidity]?.val ?? null)
398
+ : null,
399
+ temperature: plant.sensorTemperature
400
+ ? (this.sensorValues[plant.sensorTemperature]?.val ?? null)
401
+ : null,
402
+ battery: plant.sensorBattery
403
+ ? (this.sensorValues[plant.sensorBattery]?.val ?? null)
404
+ : null,
405
+ }));
406
+ }
407
+ }
408
+
409
+ module.exports = MonitorService;
package/main.js CHANGED
@@ -114,14 +114,20 @@ class FlowersAdapter extends utils.Adapter {
114
114
  try {
115
115
  if (this._checkTimer) {
116
116
  this.clearInterval(this._checkTimer);
117
+ this._checkTimer = null;
117
118
  }
118
119
  if (this._dailyReportTimer) {
119
120
  this.clearTimeout(this._dailyReportTimer);
121
+ this._dailyReportTimer = null;
120
122
  }
121
123
  if (this._weeklyReportTimer) {
122
124
  this.clearTimeout(this._weeklyReportTimer);
125
+ this._weeklyReportTimer = null;
126
+ }
127
+ if (this.monitor) {
128
+ this.monitor.unsubscribeAll().catch(() => {});
129
+ this.monitor = null;
123
130
  }
124
- this.monitor.unsubscribeAll().catch(() => {});
125
131
  this.setStateAsync("info.connection", { val: false, ack: true }).catch(
126
132
  () => {},
127
133
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.flowers",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "ioBroker adapter for monitoring indoor plants via soil moisture, temperature and battery sensors with Telegram notifications",
5
5
  "author": {
6
6
  "name": "sadam6752-tech",