homebridge-read-your-meter-pro 1.0.0-beta.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aran Shavit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # homebridge-read-your-meter-pro
2
+
3
+ Exposes water consumption from the Israeli [Read Your Meter Pro](https://rym-pro.com)
4
+ portal (ARAD meters) in HomeKit.
5
+
6
+ Unofficial, and not affiliated with Arad Group or any water corporation.
7
+
8
+ ## Read this before installing
9
+
10
+ **HomeKit has no water-consumption service.** There is no accessory type in the
11
+ HomeKit Accessory Protocol for volume or metering, so there is no honest way to
12
+ show "0.734 m³" in the Home app. This plugin works around that:
13
+
14
+ | What you get | How | Where it shows |
15
+ | --- | --- | --- |
16
+ | Today's consumption | Light sensor `Water Today` | Home app, as `734 lux` |
17
+ | This month's consumption | Light sensor `Water This Month` | Home app, as `14200 lux` |
18
+ | Month-end forecast | Light sensor `Water Forecast` | Home app |
19
+ | Cumulative meter reading | Light sensor `Water Meter Total` | Home app, optional, always m³ |
20
+ | Daily threshold exceeded | Leak sensor `Water Daily Alert` | Home app, notifications, automations |
21
+ | Monthly threshold exceeded | Leak sensor `Water Monthly Alert` | Home app, notifications, automations |
22
+
23
+ The word "lux" is wrong and there is nothing to be done about it. The number is
24
+ correct. Service names carry no unit, because HomeKit rejects names that end in
25
+ punctuation or contain symbols like `³`.
26
+
27
+ ### Why a light sensor?
28
+
29
+ This is a deliberate compromise, not an oversight. HAP defines `Valve`,
30
+ `Faucet`, `IrrigationSystem` and `LeakSensor`, but nothing that carries a volume
31
+ or a meter reading, and Apple provides no mechanism for a custom unit that the
32
+ Home app will render. The alternatives were:
33
+
34
+ - **Eve custom characteristics.** Spec-correct, but the values appear only in
35
+ the Eve app and never in Apple Home, and the `E863F131` encoding is
36
+ reverse-engineered rather than documented.
37
+ - **Show nothing numeric at all.** Leak sensors only. Honest, but users
38
+ reasonably want to glance at a number.
39
+ - **A light sensor carrying the value.** Works in every HomeKit client, no
40
+ reverse engineering, at the cost of a wrong unit label.
41
+
42
+ The third option loses one label and keeps everything else, so that is what this
43
+ plugin does. The leak sensors remain the functional core — they are a real
44
+ sensor type, so notifications and automations behave correctly.
45
+
46
+ The **leak sensors are the point**. They are a real HomeKit sensor type, so you
47
+ get push notifications and automation triggers for free — "notify me if today's
48
+ usage passes 500 L" is a decent leak detector for a house. The light sensors are
49
+ there so you can glance at a number; if you want graphs, put the data in
50
+ Prometheus/InfluxDB instead, not HomeKit.
51
+
52
+ ## Requirements
53
+
54
+ - An account at [rym-pro.com](https://rym-pro.com). Registration fails if your
55
+ meter is not an ARAD unit or your water corporation has not migrated to the
56
+ Pro portal — verify you can log in on the web before installing.
57
+ - Homebridge v1.8+ or v2.x, Node.js 22 or 24.
58
+ - No runtime dependencies, so installation is a single request — which matters
59
+ on a Raspberry Pi, where npm's per-dependency fetching is slow and failure-prone.
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ npm install -g homebridge-read-your-meter-pro
65
+ ```
66
+
67
+ Or search for "Read Your Meter Pro" in the Homebridge UI plugin browser.
68
+
69
+ ## Configuration
70
+
71
+ Use the Homebridge UI settings form, or add to `config.json`:
72
+
73
+ ```json
74
+ {
75
+ "platforms": [
76
+ {
77
+ "platform": "ReadYourMeterPro",
78
+ "name": "Read Your Meter Pro",
79
+ "email": "you@example.com",
80
+ "password": "your-portal-password",
81
+ "unit": "liters",
82
+ "pollInterval": 60,
83
+ "dailyThreshold": 500,
84
+ "monthlyThreshold": 20000,
85
+ "exposeForecast": true,
86
+ "exposeTotal": false
87
+ }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ | Option | Default | Notes |
93
+ | --- | --- | --- |
94
+ | `email` | — | Required. |
95
+ | `password` | — | Required. |
96
+ | `unit` | `liters` | `liters` or `cubic_meters`. Applies to daily/monthly/forecast. |
97
+ | `pollInterval` | `60` | Minutes. Floored at 15. |
98
+ | `dailyThreshold` | `0` | In `unit`. `0` removes the sensor. |
99
+ | `monthlyThreshold` | `0` | In `unit`. `0` removes the sensor. |
100
+ | `exposeForecast` | `true` | Month-end estimate. |
101
+ | `exposeTotal` | `false` | Cumulative reading. Always m³. |
102
+
103
+ ### Why litres by default
104
+
105
+ HomeKit clamps light level to `0.0001 .. 100000` lux. Daily consumption in m³
106
+ lands around `0.7`, which the Home app rounds to something useless. In litres it
107
+ reads as `734`. The cumulative total is the exception — a lifetime reading in
108
+ litres blows past the 100000 ceiling, so it is always reported in m³.
109
+
110
+ ### Poll politely
111
+
112
+ The meter uploads at most hourly, often daily. Polling every minute gains you
113
+ nothing and hammers someone else's infrastructure. The plugin refuses intervals
114
+ below 15 minutes.
115
+
116
+ ## Behaviour worth knowing
117
+
118
+ - **Auth**: the plugin logs in once, caches the bearer token, and re-authenticates
119
+ silently on a 401. If the portal rejects your credentials outright it stops
120
+ polling and logs an error rather than retrying on a timer and risking a lockout.
121
+ - **Credentials**: your email and password live in `config.json`, like every
122
+ other Homebridge plugin. They are never written anywhere else and never logged.
123
+ - **State**: a generated `deviceId` and the cached session token live in
124
+ `<homebridge storage>/.read-your-meter-pro.json` (mode 0600), following the
125
+ same convention as `homebridge-ring`'s `.ring.json`. The `deviceId` must stay
126
+ stable across restarts, because the portal registers a device per id. Writes
127
+ are atomic (temp file plus rename) and serialised, so a crash mid-write cannot
128
+ leave a truncated file that forces a new device registration on next boot.
129
+ Deleting the file is safe: the plugin mints a new id and logs in again.
130
+ - **Failures**: a failed poll sets `StatusFault` and clears `StatusActive` on
131
+ every service, so an outage is visible rather than silently stale. It clears on
132
+ the next successful poll. If the *first* poll after a restart fails, cached
133
+ accessories are still wired up so they report the fault instead of sitting at
134
+ default values, and the plugin retries every minute until it has something to
135
+ show before settling into the configured interval.
136
+ - **Reconfiguration**: setting a threshold to `0`, or turning off the forecast or
137
+ total, removes those services on the next restart instead of leaving ghosts.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ npm install
143
+ npm run lint
144
+ npm run build
145
+ npm test # lint + build + smoke tests against a mocked portal
146
+ ```
147
+
148
+ The smoke tests need no credentials and touch no network: they stub `fetch` with
149
+ a fake portal and drive the plugin through Homebridge's real `PlatformAccessory`
150
+ and HAP-NodeJS `Service`/`Characteristic` classes. CI runs them on Node 22 and 24.
151
+
152
+ ## Verifying the API against your account
153
+
154
+ If a field looks wrong, or the portal changes shape, dump the raw responses:
155
+
156
+ ```bash
157
+ RYM_EMAIL=you@example.com RYM_PW='your-password' npm run probe
158
+ ```
159
+
160
+ For verbose runtime logs, enable Homebridge's own debug mode (`homebridge -D`,
161
+ or the debug toggle on this plugin's child bridge in the Homebridge UI). The
162
+ plugin deliberately has no `debug` option of its own.
163
+
164
+ This prints a **redacted** summary of every endpoint the plugin uses — safe to
165
+ paste into an issue — and writes the full unredacted response to
166
+ `probe-output.json` (mode 0600, gitignored) for your own inspection. Delete that
167
+ file when you are done.
168
+
169
+ ## Credits
170
+
171
+ Endpoint knowledge derived from [pyrympro](https://github.com/OnFreund/pyrympro)
172
+ (MIT, On Freund). Originally inspired by
173
+ [read_your_meter](https://github.com/eyalcha/read_your_meter) (Apache-2.0,
174
+ eyalcha). See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
175
+
176
+ ## License
177
+
178
+ MIT
@@ -0,0 +1,51 @@
1
+ # Third-party notices
2
+
3
+ This plugin contains no third-party code. It does, however, owe its
4
+ understanding of the Read Your Meter Pro API to prior work, acknowledged here.
5
+
6
+ ## pyrympro
7
+
8
+ The endpoint layout used in `src/rympro.ts` — the base URL, the
9
+ `/consumer/login`, `/consumer/me`, `/consumption/last-read`,
10
+ `/consumption/forecast/{meter}`, `/consumption/{daily,monthly}/{meter}/{from}/{to}`
11
+ paths, the `x-access-token` header, and the meaning of login error code 5060 —
12
+ was derived from **pyrympro** by On Freund, used by the Home Assistant
13
+ `rympro` integration.
14
+
15
+ - Source: https://github.com/OnFreund/pyrympro
16
+ - License: MIT
17
+
18
+ ```
19
+ MIT License
20
+
21
+ Copyright (c) 2022 On Freund
22
+
23
+ Permission is hereby granted, free of charge, to any person obtaining a copy
24
+ of this software and associated documentation files (the "Software"), to deal
25
+ in the Software without restriction, including without limitation the rights
26
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27
+ copies of the Software, and to permit persons to whom the Software is
28
+ furnished to do so, subject to the following conditions:
29
+
30
+ The above copyright notice and this permission notice shall be included in all
31
+ copies or substantial portions of the Software.
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39
+ SOFTWARE.
40
+ ```
41
+
42
+ ## read_your_meter
43
+
44
+ The original inspiration for this plugin was **read_your_meter** by eyalcha
45
+ (Apache-2.0), a Home Assistant custom component that scraped the older
46
+ Selenium-driven portal. No code from that project is used here — it targets a
47
+ different portal by a different mechanism — but credit is due for showing the
48
+ problem was worth solving.
49
+
50
+ - Source: https://github.com/eyalcha/read_your_meter
51
+ - License: Apache-2.0
@@ -0,0 +1,113 @@
1
+ {
2
+ "pluginAlias": "ReadYourMeterPro",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Exposes your Read Your Meter Pro water meter in HomeKit. HomeKit has no water-consumption service, so readings are surfaced as **light sensors** (the number is your consumption, the unit says \"lux\" \u2014 ignore it) and thresholds as **leak sensors** you can use for automations and notifications.",
6
+ "footerDisplay": "For verbose logs, enable Homebridge debug mode (or the debug toggle on this plugin's child bridge). Unofficial; not affiliated with Arad Group or any water corporation.",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Name",
12
+ "type": "string",
13
+ "default": "Read Your Meter Pro",
14
+ "required": true
15
+ },
16
+ "email": {
17
+ "title": "Email",
18
+ "type": "string",
19
+ "format": "email",
20
+ "required": true,
21
+ "description": "The email address you use to sign in at rym-pro.com."
22
+ },
23
+ "password": {
24
+ "title": "Password",
25
+ "type": "string",
26
+ "required": true,
27
+ "x-schema-form": {
28
+ "type": "password"
29
+ }
30
+ },
31
+ "pollInterval": {
32
+ "title": "Poll interval (minutes)",
33
+ "type": "integer",
34
+ "default": 60,
35
+ "minimum": 15,
36
+ "maximum": 1440,
37
+ "description": "The meter itself only reports hourly at best, so there is nothing to gain below 60."
38
+ },
39
+ "unit": {
40
+ "title": "Unit",
41
+ "type": "string",
42
+ "default": "liters",
43
+ "oneOf": [
44
+ {
45
+ "title": "Litres",
46
+ "enum": [
47
+ "liters"
48
+ ]
49
+ },
50
+ {
51
+ "title": "Cubic metres (m\u00b3)",
52
+ "enum": [
53
+ "cubic_meters"
54
+ ]
55
+ }
56
+ ],
57
+ "description": "Litres read better in the Home app: 734 instead of 0.734."
58
+ },
59
+ "dailyThreshold": {
60
+ "title": "Daily alert threshold",
61
+ "type": "number",
62
+ "default": 0,
63
+ "minimum": 0,
64
+ "description": "Trips the daily leak sensor when today's consumption reaches this value, in the unit selected above. 0 disables the sensor."
65
+ },
66
+ "monthlyThreshold": {
67
+ "title": "Monthly alert threshold",
68
+ "type": "number",
69
+ "default": 0,
70
+ "minimum": 0,
71
+ "description": "Trips the monthly leak sensor when this month's consumption reaches this value. 0 disables the sensor."
72
+ },
73
+ "exposeForecast": {
74
+ "title": "Expose month-end forecast",
75
+ "type": "boolean",
76
+ "default": true
77
+ },
78
+ "exposeTotal": {
79
+ "title": "Expose cumulative meter reading",
80
+ "type": "boolean",
81
+ "default": false,
82
+ "description": "Always reported in m\u00b3, since a lifetime total in litres exceeds HomeKit's 100000 ceiling."
83
+ }
84
+ }
85
+ },
86
+ "layout": [
87
+ "email",
88
+ "password",
89
+ {
90
+ "type": "fieldset",
91
+ "title": "Alerts",
92
+ "expandable": true,
93
+ "expanded": true,
94
+ "items": [
95
+ "dailyThreshold",
96
+ "monthlyThreshold"
97
+ ]
98
+ },
99
+ {
100
+ "type": "fieldset",
101
+ "title": "Advanced",
102
+ "expandable": true,
103
+ "expanded": false,
104
+ "items": [
105
+ "name",
106
+ "unit",
107
+ "pollInterval",
108
+ "exposeForecast",
109
+ "exposeTotal"
110
+ ]
111
+ }
112
+ ]
113
+ }
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { RymProPlatform } from './platform.js';
2
+ import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
3
+ export default (api) => {
4
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, RymProPlatform);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE3D,eAAe,CAAC,GAAQ,EAAQ,EAAE;IAChC,GAAG,CAAC,gBAAgB,CAAC,WAAW,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;AACnE,CAAC,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { PlatformAccessory } from 'homebridge';
2
+ import type { RymProPlatform } from './platform.js';
3
+ import type { MeterSnapshot } from './rympro.js';
4
+ import { type ResolvedConfig } from './settings.js';
5
+ export declare class MeterAccessory {
6
+ private readonly platform;
7
+ readonly accessory: PlatformAccessory;
8
+ private readonly config;
9
+ private readonly services;
10
+ private readonly unit;
11
+ private readonly information;
12
+ constructor(platform: RymProPlatform, accessory: PlatformAccessory, config: ResolvedConfig);
13
+ /** Which services this accessory should expose, given the current config. */
14
+ private desiredServices;
15
+ private buildServices;
16
+ update(snapshot: MeterSnapshot): void;
17
+ /** Flags every service as faulted so the failure is visible in the Home app. */
18
+ setFaulted(): void;
19
+ private setLux;
20
+ private setLeak;
21
+ }
22
+ /**
23
+ * HomeKit rejects values outside 0.0001..100000 lux, and a real zero reading is
24
+ * common right after midnight, so genuine zeroes have to floor at 0.0001.
25
+ */
26
+ export declare function clampLux(value: number): number;
@@ -0,0 +1,162 @@
1
+ import { LUX_MAX, LUX_MIN } from './settings.js';
2
+ export class MeterAccessory {
3
+ platform;
4
+ accessory;
5
+ config;
6
+ services = new Map();
7
+ unit;
8
+ information;
9
+ constructor(platform, accessory, config) {
10
+ this.platform = platform;
11
+ this.accessory = accessory;
12
+ this.config = config;
13
+ this.unit = config.unit;
14
+ const { Service, Characteristic } = platform;
15
+ const meterCount = accessory.context.meterCount;
16
+ this.information = (accessory.getService(Service.AccessoryInformation) ??
17
+ accessory.addService(Service.AccessoryInformation))
18
+ .setCharacteristic(Characteristic.Manufacturer, 'Arad / Read Your Meter Pro')
19
+ .setCharacteristic(Characteristic.Model, 'Water Meter')
20
+ .setCharacteristic(Characteristic.SerialNumber, String(meterCount));
21
+ this.buildServices();
22
+ }
23
+ /** Which services this accessory should expose, given the current config. */
24
+ desiredServices() {
25
+ // HomeKit requires names to start and end with a letter or digit, and
26
+ // rejects symbols like the superscript in "m³" — so no units in the name.
27
+ // The unit is a config choice and is documented in the plugin settings.
28
+ const specs = [
29
+ { subtype: 'daily', name: 'Water Today', kind: 'light' },
30
+ { subtype: 'monthly', name: 'Water This Month', kind: 'light' },
31
+ ];
32
+ if (this.config.exposeForecast) {
33
+ specs.push({ subtype: 'forecast', name: 'Water Forecast', kind: 'light' });
34
+ }
35
+ if (this.config.exposeTotal) {
36
+ specs.push({ subtype: 'total', name: 'Water Meter Total', kind: 'light' });
37
+ }
38
+ if (this.config.dailyThreshold > 0) {
39
+ specs.push({ subtype: 'daily-alert', name: 'Water Daily Alert', kind: 'leak' });
40
+ }
41
+ if (this.config.monthlyThreshold > 0) {
42
+ specs.push({ subtype: 'monthly-alert', name: 'Water Monthly Alert', kind: 'leak' });
43
+ }
44
+ return specs;
45
+ }
46
+ buildServices() {
47
+ const { Service, Characteristic } = this.platform;
48
+ const specs = this.desiredServices();
49
+ const wanted = new Set(specs.map((s) => s.subtype));
50
+ // Remove services left over from a previous config (e.g. a threshold that
51
+ // was set to 0, or the forecast sensor being switched off).
52
+ for (const service of [...this.accessory.services]) {
53
+ const subtype = service.subtype;
54
+ if (service.UUID !== Service.AccessoryInformation.UUID &&
55
+ subtype !== undefined &&
56
+ !wanted.has(subtype)) {
57
+ this.platform.log.info(`Removing no-longer-configured service "${service.displayName}" from ${this.accessory.displayName}`);
58
+ this.accessory.removeService(service);
59
+ }
60
+ }
61
+ for (const spec of specs) {
62
+ const type = spec.kind === 'light' ? Service.LightSensor : Service.LeakSensor;
63
+ const service = this.accessory.getServiceById(type, spec.subtype) ??
64
+ this.accessory.addService(type, spec.name, spec.subtype);
65
+ service.setCharacteristic(Characteristic.Name, spec.name);
66
+ if (Characteristic.ConfiguredName) {
67
+ // Lets the Home app show a sane per-service label instead of "Sensor".
68
+ if (!service.testCharacteristic(Characteristic.ConfiguredName)) {
69
+ service.addOptionalCharacteristic(Characteristic.ConfiguredName);
70
+ }
71
+ service.updateCharacteristic(Characteristic.ConfiguredName, spec.name);
72
+ }
73
+ service.setCharacteristic(Characteristic.StatusActive, true);
74
+ service.setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT);
75
+ this.services.set(spec.subtype, service);
76
+ }
77
+ }
78
+ update(snapshot) {
79
+ const { Characteristic } = this.platform;
80
+ const factor = this.unit === 'liters' ? 1000 : 1;
81
+ const scale = (v) => (v === null ? null : v * factor);
82
+ const daily = scale(snapshot.daily);
83
+ const monthly = scale(snapshot.monthly);
84
+ // A null reading means "not published yet", not "zero". Leaving the last
85
+ // known value in place beats flashing a zero every morning; the very first
86
+ // poll has nothing to keep, so it floors instead.
87
+ this.setLux('daily', daily);
88
+ this.setLux('monthly', monthly);
89
+ this.setLux('forecast', scale(snapshot.forecast));
90
+ // Total is always m³: a cumulative reading in litres blows past the
91
+ // 100000 lux ceiling within a couple of years of normal household use.
92
+ this.setLux('total', snapshot.total);
93
+ // With no reading there is nothing to compare, so the alert stays clear
94
+ // rather than tripping or clearing on invented data.
95
+ if (daily !== null) {
96
+ if (snapshot.serial) {
97
+ // The physical serial only arrives with the first poll, so it cannot be
98
+ // set in the constructor.
99
+ this.information.updateCharacteristic(this.platform.Characteristic.SerialNumber, snapshot.serial);
100
+ }
101
+ this.setLeak('daily-alert', daily >= this.config.dailyThreshold);
102
+ }
103
+ if (monthly !== null) {
104
+ this.setLeak('monthly-alert', monthly >= this.config.monthlyThreshold);
105
+ }
106
+ for (const service of this.services.values()) {
107
+ service.updateCharacteristic(Characteristic.StatusActive, true);
108
+ service.updateCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT);
109
+ }
110
+ const unitLabel = this.unit === 'liters' ? 'L' : 'm³';
111
+ const show = (v) => (v === null ? 'no reading yet' : `${round(v)}${unitLabel}`);
112
+ this.platform.log.debug(`Meter ${snapshot.meterCount}: today=${show(daily)} month=${show(monthly)} ` +
113
+ `forecast=${show(scale(snapshot.forecast))} total=${round(snapshot.total)}m³`);
114
+ }
115
+ /** Flags every service as faulted so the failure is visible in the Home app. */
116
+ setFaulted() {
117
+ const { Characteristic } = this.platform;
118
+ for (const service of this.services.values()) {
119
+ service.updateCharacteristic(Characteristic.StatusActive, false);
120
+ service.updateCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.GENERAL_FAULT);
121
+ }
122
+ }
123
+ setLux(subtype, value) {
124
+ const service = this.services.get(subtype);
125
+ if (!service) {
126
+ return;
127
+ }
128
+ if (value === null) {
129
+ // Keep whatever was last published rather than reporting a false zero.
130
+ return;
131
+ }
132
+ if (value > LUX_MAX) {
133
+ this.platform.log.warn(`${service.displayName}: ${round(value)} exceeds HomeKit's 100000 lux ceiling and was clamped. ` +
134
+ 'Switch "unit" to cubic_meters if this keeps happening.');
135
+ }
136
+ service.updateCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel, clampLux(value));
137
+ }
138
+ setLeak(subtype, tripped) {
139
+ const service = this.services.get(subtype);
140
+ if (!service) {
141
+ return;
142
+ }
143
+ const { Characteristic } = this.platform;
144
+ service.updateCharacteristic(Characteristic.LeakDetected, tripped
145
+ ? Characteristic.LeakDetected.LEAK_DETECTED
146
+ : Characteristic.LeakDetected.LEAK_NOT_DETECTED);
147
+ }
148
+ }
149
+ /**
150
+ * HomeKit rejects values outside 0.0001..100000 lux, and a real zero reading is
151
+ * common right after midnight, so genuine zeroes have to floor at 0.0001.
152
+ */
153
+ export function clampLux(value) {
154
+ if (!Number.isFinite(value)) {
155
+ return LUX_MIN;
156
+ }
157
+ return Math.min(LUX_MAX, Math.max(LUX_MIN, value));
158
+ }
159
+ function round(value) {
160
+ return Math.round(value * 1000) / 1000;
161
+ }
162
+ //# sourceMappingURL=meterAccessory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meterAccessory.js","sourceRoot":"","sources":["../src/meterAccessory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { API, Characteristic, DynamicPlatformPlugin, Logging, PlatformAccessory, Service } from 'homebridge';
2
+ import { type ResolvedConfig, type RymProPlatformConfig } from './settings.js';
3
+ export declare class RymProPlatform implements DynamicPlatformPlugin {
4
+ readonly log: Logging;
5
+ readonly api: API;
6
+ readonly Service: typeof Service;
7
+ readonly Characteristic: typeof Characteristic;
8
+ /** Accessories restored from Homebridge's cache, keyed by UUID. */
9
+ private readonly cachedAccessories;
10
+ private readonly meters;
11
+ readonly settings: ResolvedConfig | null;
12
+ private client;
13
+ private timer;
14
+ private stopped;
15
+ private readonly statePath;
16
+ /** In-memory source of truth; disk is only read once, at startup. */
17
+ private state;
18
+ /** Serialises writes so a fire-and-forget save can't interleave with another. */
19
+ private writeQueue;
20
+ private hadSuccessfulPoll;
21
+ constructor(log: Logging, config: RymProPlatformConfig, api: API);
22
+ /** Called by Homebridge for each accessory restored from disk cache. */
23
+ configureAccessory(accessory: PlatformAccessory): void;
24
+ private start;
25
+ private poll;
26
+ private scheduleNext;
27
+ /**
28
+ * Wires up handlers for accessories restored from cache that have not been
29
+ * matched to a live meter yet. Without this, a failed first poll after a
30
+ * restart leaves cached accessories visible in the Home app with default
31
+ * values and no fault indication.
32
+ */
33
+ private adoptCachedAccessories;
34
+ private syncAccessories;
35
+ private markFaulted;
36
+ private loadState;
37
+ /**
38
+ * Merges a patch into the in-memory state and queues an atomic write.
39
+ * Writes are chained rather than fired in parallel: `writeFile` truncates
40
+ * before it writes, so two concurrent saves can leave a half-written file
41
+ * that fails to parse on the next start.
42
+ */
43
+ private persist;
44
+ /** Test hook: resolves once every queued write has landed. */
45
+ flushState(): Promise<void>;
46
+ }