iobroker.flowers 0.3.2 → 0.3.4
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 +18 -0
- package/io-package.json +27 -27
- package/lib/monitor-service.js +409 -383
- package/main.js +7 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
Monitor indoor plants via soil moisture, temperature and battery sensors with Telegram notifications.
|
|
9
9
|
|
|
10
|
+
This adapter works with **any sensor** already integrated in ioBroker (Zigbee, Wi-Fi, Bluetooth, Z-Wave, etc.) — no specific hardware required. Popular compatible sensors:
|
|
11
|
+
|
|
12
|
+
- [Xiaomi Mi Flora / HHCC Plant Sensor](https://www.mi.com/global/mi-flora) — Bluetooth soil moisture + temperature + light
|
|
13
|
+
- [Zigbee soil moisture sensors](https://www.zigbee2mqtt.io/supported-devices/#s=soil) (e.g. Tuya TS0601, MOES) — via ioBroker Zigbee adapter
|
|
14
|
+
- Any sensor exposing humidity/temperature/battery states in ioBroker
|
|
15
|
+
|
|
16
|
+
Notifications are sent via the [ioBroker Telegram adapter](https://github.com/ioBroker/ioBroker.telegram).
|
|
17
|
+
|
|
10
18
|
## Features
|
|
11
19
|
|
|
12
20
|
- Monitor multiple plants with individual sensor assignments
|
|
@@ -75,6 +83,16 @@ Only one watering cycle runs at a time per plant. Configure the duration in Sett
|
|
|
75
83
|
|
|
76
84
|
## Changelog
|
|
77
85
|
|
|
86
|
+
### 0.3.4 (2026-03-31)
|
|
87
|
+
- (sadam6752-tech) Add unit tests for MonitorService, NotificationManager and messages (106 tests total)
|
|
88
|
+
- (sadam6752-tech) Update README with links to compatible devices and Telegram adapter
|
|
89
|
+
- (sadam6752-tech) Remove mocha from devDependencies (already included in @iobroker/testing)
|
|
90
|
+
|
|
91
|
+
### 0.3.3 (2026-03-30)
|
|
92
|
+
- (sadam6752-tech) Fix object hierarchy: create device/channel parent objects before states
|
|
93
|
+
- (sadam6752-tech) Use correct state roles: value.humidity, value.temperature, value.battery
|
|
94
|
+
- (sadam6752-tech) Improve unload: null timers after clearing, guard monitor null check
|
|
95
|
+
|
|
78
96
|
### 0.3.2 (2026-03-30)
|
|
79
97
|
- (sadam6752-tech) Custom profiles: users can create own plant profiles in Profiles tab
|
|
80
98
|
- (sadam6752-tech) Custom profile field in Plants table for direct profile name entry
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "flowers",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.3.4": {
|
|
7
|
+
"en": "Add unit tests for MonitorService, NotificationManager and messages; update README with device links; remove mocha from devDependencies",
|
|
8
|
+
"de": "Unit-Tests für MonitorService, NotificationManager und messages hinzugefügt; README mit Gerätelinks aktualisiert; mocha aus devDependencies entfernt",
|
|
9
|
+
"ru": "Добавлены unit-тесты для MonitorService, NotificationManager и messages; обновлён README со ссылками на устройства; удалён mocha из devDependencies",
|
|
10
|
+
"fr": "Tests unitaires ajoutés pour MonitorService, NotificationManager et messages; README mis à jour avec liens appareils; mocha supprimé des devDependencies",
|
|
11
|
+
"it": "Aggiunti unit test per MonitorService, NotificationManager e messages; README aggiornato con link dispositivi; rimosso mocha da devDependencies",
|
|
12
|
+
"es": "Tests unitarios añadidos para MonitorService, NotificationManager y messages; README actualizado con enlaces de dispositivos; mocha eliminado de devDependencies",
|
|
13
|
+
"pl": "Dodano testy jednostkowe dla MonitorService, NotificationManager i messages; zaktualizowano README z linkami do urządzeń; usunięto mocha z devDependencies",
|
|
14
|
+
"pt": "Testes unitários adicionados para MonitorService, NotificationManager e messages; README atualizado com links de dispositivos; mocha removido de devDependencies",
|
|
15
|
+
"nl": "Unit-tests toegevoegd voor MonitorService, NotificationManager en messages; README bijgewerkt met apparaatlinks; mocha verwijderd uit devDependencies",
|
|
16
|
+
"uk": "Додано unit-тести для MonitorService, NotificationManager та messages; оновлено README з посиланнями на пристрої; видалено mocha з devDependencies",
|
|
17
|
+
"zh-cn": "为MonitorService、NotificationManager和messages添加单元测试;更新README添加设备链接;从devDependencies中删除mocha"
|
|
18
|
+
},
|
|
19
|
+
"0.3.3": {
|
|
20
|
+
"en": "Fix object hierarchy (device/channel/state), correct state roles (value.humidity, value.temperature, value.battery), improve unload cleanup",
|
|
21
|
+
"de": "Objekthierarchie korrigiert (device/channel/state), korrekte State-Rollen, verbessertes Aufräumen beim Entladen",
|
|
22
|
+
"ru": "Исправлена иерархия объектов (device/channel/state), правильные роли состояний, улучшена очистка при выгрузке",
|
|
23
|
+
"fr": "Correction hiérarchie objets (device/channel/state), rôles d'état corrects, nettoyage amélioré au déchargement",
|
|
24
|
+
"it": "Corretta gerarchia oggetti (device/channel/state), ruoli stato corretti, pulizia migliorata allo scaricamento",
|
|
25
|
+
"es": "Corrección jerarquía objetos (device/channel/state), roles de estado correctos, limpieza mejorada al descargar",
|
|
26
|
+
"pl": "Poprawiono hierarchię obiektów (device/channel/state), poprawne role stanów, ulepszone czyszczenie przy wyładowaniu",
|
|
27
|
+
"pt": "Correção hierarquia objetos (device/channel/state), funções de estado corretas, limpeza melhorada no descarregamento",
|
|
28
|
+
"nl": "Objecthiërarchie gecorrigeerd (device/channel/state), correcte state-rollen, verbeterde opruiming bij ontladen",
|
|
29
|
+
"uk": "Виправлено ієрархію об'єктів (device/channel/state), правильні ролі станів, покращено очищення при вивантаженні",
|
|
30
|
+
"zh-cn": "修复对象层次结构(device/channel/state),正确的状态角色,改进卸载清理"
|
|
31
|
+
},
|
|
6
32
|
"0.3.2": {
|
|
7
33
|
"en": "Custom profiles: users can create own plant profiles in Profiles tab",
|
|
8
34
|
"de": "Benutzerdefinierte Profile: eigene Pflanzenprofile in der Profiles-Registerkarte erstellen",
|
|
@@ -16,32 +42,6 @@
|
|
|
16
42
|
"uk": "Користувацькі профілі: створення власних профілів рослин у вкладці Profiles",
|
|
17
43
|
"zh-cn": "自定义配置文件:在配置文件选项卡中创建自己的植物配置文件"
|
|
18
44
|
},
|
|
19
|
-
"0.3.1": {
|
|
20
|
-
"en": "Fixed all lint warnings: complete JSDoc descriptions",
|
|
21
|
-
"de": "Alle Lint-Warnungen behoben: vollständige JSDoc-Beschreibungen",
|
|
22
|
-
"ru": "Исправлены все lint-предупреждения: полные JSDoc-описания",
|
|
23
|
-
"fr": "Tous les avertissements lint corrigés: descriptions JSDoc complètes",
|
|
24
|
-
"it": "Tutti gli avvisi lint corretti: descrizioni JSDoc complete",
|
|
25
|
-
"es": "Todas las advertencias lint corregidas: descripciones JSDoc completas",
|
|
26
|
-
"pl": "Naprawiono wszystkie ostrzeżenia lint: pełne opisy JSDoc",
|
|
27
|
-
"pt": "Todos os avisos lint corrigidos: descrições JSDoc completas",
|
|
28
|
-
"nl": "Alle lint-waarschuwingen opgelost: volledige JSDoc-beschrijvingen",
|
|
29
|
-
"uk": "Виправлено всі lint-попередження: повні JSDoc-описи",
|
|
30
|
-
"zh-cn": "修复所有lint警告:完整的JSDoc描述"
|
|
31
|
-
},
|
|
32
|
-
"0.3.0": {
|
|
33
|
-
"en": "Standard CI/CD workflow, dependabot, release-script, standard test structure",
|
|
34
|
-
"de": "Standard CI/CD Workflow, Dependabot, Release-Script, Standard-Teststruktur",
|
|
35
|
-
"ru": "Стандартный CI/CD workflow, dependabot, release-script, стандартная структура тестов",
|
|
36
|
-
"fr": "Workflow CI/CD standard, dependabot, release-script, structure de tests standard",
|
|
37
|
-
"it": "Workflow CI/CD standard, dependabot, release-script, struttura test standard",
|
|
38
|
-
"es": "Workflow CI/CD estándar, dependabot, release-script, estructura de tests estándar",
|
|
39
|
-
"pl": "Standardowy workflow CI/CD, dependabot, release-script, standardowa struktura testów",
|
|
40
|
-
"pt": "Workflow CI/CD padrão, dependabot, release-script, estrutura de testes padrão",
|
|
41
|
-
"nl": "Standaard CI/CD workflow, dependabot, release-script, standaard teststructuur",
|
|
42
|
-
"uk": "Стандартний CI/CD workflow, dependabot, release-script, стандартна структура тестів",
|
|
43
|
-
"zh-cn": "标准CI/CD工作流、dependabot、release-script、标准测试结构"
|
|
44
|
-
},
|
|
45
45
|
"0.2.9": {
|
|
46
46
|
"en": "Automatic watering support; UI column width fixes",
|
|
47
47
|
"de": "Automatische Bewässerung; UI-Spaltenbreiten korrigiert",
|
package/lib/monitor-service.js
CHANGED
|
@@ -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
|
-
*
|
|
324
|
-
* @param {
|
|
325
|
-
* @param {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
"@iobroker/dev-server": "^0.8.0",
|
|
36
36
|
"@iobroker/eslint-config": "^2.2.0",
|
|
37
37
|
"@iobroker/testing": "^5.2.2",
|
|
38
|
-
"eslint": "^9.17.0"
|
|
39
|
-
"mocha": "^11.7.5"
|
|
38
|
+
"eslint": "^9.17.0"
|
|
40
39
|
},
|
|
41
40
|
"main": "main.js",
|
|
42
41
|
"files": [
|