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 +21 -0
- package/README.md +178 -0
- package/THIRD-PARTY-NOTICES.md +51 -0
- package/config.schema.json +113 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/meterAccessory.d.ts +26 -0
- package/dist/meterAccessory.js +162 -0
- package/dist/meterAccessory.js.map +1 -0
- package/dist/platform.d.ts +46 -0
- package/dist/platform.js +253 -0
- package/dist/platform.js.map +1 -0
- package/dist/rympro.d.ts +58 -0
- package/dist/rympro.js +185 -0
- package/dist/rympro.js.map +1 -0
- package/dist/settings.d.ts +47 -0
- package/dist/settings.js +45 -0
- package/dist/settings.js.map +1 -0
- package/package.json +58 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
}
|