homebridge-google-nest-sdm-v2 2.0.0
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/CHANGELOG.md +42 -0
- package/LICENSE +15 -0
- package/README.md +143 -0
- package/config.schema.json +114 -0
- package/dist/Accessory.js +22 -0
- package/dist/Accessory.js.map +1 -0
- package/dist/CameraAccessory.js +17 -0
- package/dist/CameraAccessory.js.map +1 -0
- package/dist/CameraStreamingDelegate.js +15 -0
- package/dist/CameraStreamingDelegate.js.map +1 -0
- package/dist/Config.js +3 -0
- package/dist/Config.js.map +1 -0
- package/dist/DoorbellAccessory.js +22 -0
- package/dist/DoorbellAccessory.js.map +1 -0
- package/dist/DoorbellStreamingDelegate.js +15 -0
- package/dist/DoorbellStreamingDelegate.js.map +1 -0
- package/dist/EcoMode.js +16 -0
- package/dist/EcoMode.js.map +1 -0
- package/dist/FanAccessory.js +77 -0
- package/dist/FanAccessory.js.map +1 -0
- package/dist/FfMpegProcess.js +90 -0
- package/dist/FfMpegProcess.js.map +1 -0
- package/dist/HksvStreamer.js +166 -0
- package/dist/HksvStreamer.js.map +1 -0
- package/dist/MotionAccessory.js +34 -0
- package/dist/MotionAccessory.js.map +1 -0
- package/dist/NestStreamer.js +181 -0
- package/dist/NestStreamer.js.map +1 -0
- package/dist/Platform.js +190 -0
- package/dist/Platform.js.map +1 -0
- package/dist/Settings.js +12 -0
- package/dist/Settings.js.map +1 -0
- package/dist/SnapshotRefresher.js +223 -0
- package/dist/SnapshotRefresher.js.map +1 -0
- package/dist/StreamingDelegate.js +494 -0
- package/dist/StreamingDelegate.js.map +1 -0
- package/dist/ThermostatAccessory.js +382 -0
- package/dist/ThermostatAccessory.js.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/res/google-logo.jpg +0 -0
- package/dist/res/nest-logo.jpg +0 -0
- package/dist/sdm/Api.js +126 -0
- package/dist/sdm/Api.js.map +1 -0
- package/dist/sdm/Camera.js +183 -0
- package/dist/sdm/Camera.js.map +1 -0
- package/dist/sdm/Commands.js +18 -0
- package/dist/sdm/Commands.js.map +1 -0
- package/dist/sdm/Device.js +76 -0
- package/dist/sdm/Device.js.map +1 -0
- package/dist/sdm/Display.js +14 -0
- package/dist/sdm/Display.js.map +1 -0
- package/dist/sdm/Doorbell.js +63 -0
- package/dist/sdm/Doorbell.js.map +1 -0
- package/dist/sdm/Events.js +44 -0
- package/dist/sdm/Events.js.map +1 -0
- package/dist/sdm/Responses.js +3 -0
- package/dist/sdm/Responses.js.map +1 -0
- package/dist/sdm/Thermostat.js +249 -0
- package/dist/sdm/Thermostat.js.map +1 -0
- package/dist/sdm/Traits.js +65 -0
- package/dist/sdm/Traits.js.map +1 -0
- package/dist/sdm/Types.js +3 -0
- package/dist/sdm/Types.js.map +1 -0
- package/dist/sdm/UnknownDevice.js +13 -0
- package/dist/sdm/UnknownDevice.js.map +1 -0
- package/dist/util.js +29 -0
- package/dist/util.js.map +1 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this fork are documented here. This project is a maintained
|
|
4
|
+
fork of [`homebridge-google-nest-sdm`](https://github.com/potmat/homebridge-google-nest-sdm)
|
|
5
|
+
by potmat; it follows the same ISC license.
|
|
6
|
+
|
|
7
|
+
## 2.0.0
|
|
8
|
+
|
|
9
|
+
First release of the `homebridge-google-nest-sdm-v2` fork, based on upstream `1.1.23`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Refreshing camera snapshots.** Home-app tiles show a recent frame instead of the
|
|
13
|
+
static Nest/Google placeholder. Stills are grabbed on motion/person events and
|
|
14
|
+
(optionally) when the Home app requests snapshots, via a single serialized,
|
|
15
|
+
rate-limited queue. New options: `snapshotRefresh`, `snapshotRefreshOnAppOpen`,
|
|
16
|
+
`snapshotRefreshSpacing`, `snapshotRefreshTtl`. (upstream #45)
|
|
17
|
+
- **`ffmpegPath` option** to use a custom ffmpeg binary (e.g. a hardware-accelerated
|
|
18
|
+
build for `h264_qsv` / `h264_vaapi` / `h264_nvenc` / `h264_videotoolbox`), instead of
|
|
19
|
+
being locked to the bundled binary.
|
|
20
|
+
|
|
21
|
+
### Fixed / Hardened
|
|
22
|
+
- **Security:** SDM command/list/event-image errors no longer log the full Axios error
|
|
23
|
+
object, which contained the OAuth `Authorization: Bearer` token (and cookies). Errors
|
|
24
|
+
are now summarized to status + message only. (relates to upstream #99)
|
|
25
|
+
- **Graceful 429 handling:** a `RESOURCE_EXHAUSTED` on WebRTC stream start now throws a
|
|
26
|
+
clear "rate-limited" error instead of `TypeError: Cannot read properties of undefined
|
|
27
|
+
(reading 'mediaSessionId')`. (upstream #99)
|
|
28
|
+
- **HKSV teardown:** recording streams now escalate to SIGKILL and always close the
|
|
29
|
+
server/socket, and the recording generator tears down in a `finally` — reducing
|
|
30
|
+
orphaned ffmpeg processes and growing memory use. (upstream #150)
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Homebridge v2 support:** `engines.homebridge` is now `^1.6.0 || ^2.0.0`. (upstream #200)
|
|
34
|
+
- Snapshot cache is stored under the Homebridge storage path (`api.user.storagePath()`),
|
|
35
|
+
which is correct even when Homebridge runs under a non-login service account
|
|
36
|
+
(e.g. Windows LocalSystem), rather than `os.homedir()`.
|
|
37
|
+
- Clearer config field descriptions and README setup guide: disambiguates the three
|
|
38
|
+
"project" identifiers and foregrounds the required Pub/Sub OAuth scope.
|
|
39
|
+
|
|
40
|
+
### Attribution
|
|
41
|
+
- Original plugin © potmat; Homebridge plugin template © 2020 Andreas Bauer. ISC license
|
|
42
|
+
retained. This fork is maintained on a best-effort basis and is not affiliated with Google.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License (ISC)
|
|
2
|
+
Copyright (c) 2020 Andreas Bauer
|
|
3
|
+
Copyright (c) 2026 Zac (homebridge-google-nest-sdm-v2 fork)
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# homebridge-google-nest-sdm-v2
|
|
2
|
+
|
|
3
|
+
A Homebridge plugin for Google Nest **cameras, doorbells, displays, and thermostats** via the documented [Google Smart Device Management (SDM) API](https://developers.google.com/nest/device-access). Supports HomeKit Secure Video.
|
|
4
|
+
|
|
5
|
+
> **Maintained fork.** This is a community-maintained continuation of the excellent [`homebridge-google-nest-sdm`](https://github.com/potmat/homebridge-google-nest-sdm) by **potmat** (originally based on a Homebridge template © 2020 Andreas Bauer), which is no longer actively updated. All credit for the original work goes to them; this fork is offered **best-effort** under the same ISC license. It is not affiliated with or endorsed by Google.
|
|
6
|
+
|
|
7
|
+
## What's new in v2
|
|
8
|
+
|
|
9
|
+
- **Refreshing camera snapshots** — Home-app tiles now show a recent frame instead of the generic Nest/Google placeholder. Stills are grabbed on motion and (optionally) when you open the Home app, through a single **rate-limited, serialized queue** so it stays clear of the SDM 429 limit. Configurable; can be turned off. *(addresses [#45](https://github.com/potmat/homebridge-google-nest-sdm/issues/45))*
|
|
10
|
+
- **Configurable ffmpeg / hardware transcoding** — a new `ffmpegPath` option lets you point at a build with the hardware encoder you want (`h264_qsv`, `h264_videotoolbox`, `h264_vaapi`, `h264_nvenc`, …) instead of being locked to the bundled binary.
|
|
11
|
+
- **Security: no more tokens in logs** — SDM command errors no longer dump the full request (which contained your OAuth `Authorization: Bearer …` token) into the Homebridge log. *(addresses the leak seen in [#99](https://github.com/potmat/homebridge-google-nest-sdm/issues/99))*
|
|
12
|
+
- **Graceful rate-limit handling** — an SDM `429 RESOURCE_EXHAUSTED` on stream start now logs a clear message instead of throwing `TypeError: Cannot read properties of undefined (reading 'mediaSessionId')`. *(addresses [#99](https://github.com/potmat/homebridge-google-nest-sdm/issues/99))*
|
|
13
|
+
- **HKSV teardown hardening** — recording streams now force-kill ffmpeg (SIGKILL escalation) and always release the server/socket, reducing orphaned ffmpeg processes / growing memory. *(addresses [#150](https://github.com/potmat/homebridge-google-nest-sdm/issues/150))*
|
|
14
|
+
- **Homebridge v2 support** — declared compatible with Homebridge `^1.6.0 || ^2.0.0`. *(addresses [#200](https://github.com/potmat/homebridge-google-nest-sdm/issues/200))*
|
|
15
|
+
- **Clearer setup docs** — the three "project" identifiers are disambiguated, and the critical Pub/Sub-scope step is front and center (see below).
|
|
16
|
+
|
|
17
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm install -g --unsafe-perm homebridge-google-nest-sdm-v2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install **Google Nest SDM (v2)** from the Homebridge UI plugin search. Don't forget `--unsafe-perm` on the CLI.
|
|
26
|
+
|
|
27
|
+
> Migrating from the original `homebridge-google-nest-sdm`? Uninstall it first (they register the same platform and can't both run), then install this. Your existing config works as-is — the `"platform"` value stays `"homebridge-google-nest-sdm"`.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
You need five values from Google. Follow Google's [Device Access getting-started guide](https://developers.google.com/nest/device-access/get-started), then mind the **Pub/Sub scope** step below.
|
|
32
|
+
|
|
33
|
+
### The three "project" identifiers (this trips everyone up)
|
|
34
|
+
|
|
35
|
+
Google's setup involves **three different things all called a "project."** Mixing them up is the #1 cause of setup failure (typically an `invalid_client` error):
|
|
36
|
+
|
|
37
|
+
| Config field | What it is | Looks like | Where it comes from |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `projectId` | **Device Access** Project ID | a UUID, `4f689e03-...` | [Device Access Console](https://developers.google.com/nest/device-access/get-started#create_a_device_access_project) |
|
|
40
|
+
| `gcpProjectId` | **Google Cloud** Project ID | `my-project-334315` | the GCP project where you made the OAuth client + Pub/Sub subscription |
|
|
41
|
+
| (not a config field) | GCP **project number** | `837325688835` | appears only in some error messages — same project as `gcpProjectId` |
|
|
42
|
+
|
|
43
|
+
And the OAuth credentials, which are **not** any of the above:
|
|
44
|
+
|
|
45
|
+
- `clientId` — OAuth client ID, ends in `.apps.googleusercontent.com` ([GCP setup](https://developers.google.com/nest/device-access/get-started#set_up_google_cloud_platform)).
|
|
46
|
+
- `clientSecret` — paired secret, starts with `GOCSPX-`.
|
|
47
|
+
|
|
48
|
+
### Remaining values
|
|
49
|
+
|
|
50
|
+
- `refreshToken` — from [authorizing the account](https://developers.google.com/nest/device-access/authorize#get_an_access_token). **Must be generated with the Pub/Sub scope** (see below).
|
|
51
|
+
- `subscriptionId` — from [creating a Pub/Sub pull subscription](https://developers.google.com/nest/device-access/subscribe-to-events#create_a_pull_subscription). Full path form: `projects/<gcp-project-id>/subscriptions/<subscription-id>`.
|
|
52
|
+
|
|
53
|
+
### ⚠️ THE PUB/SUB SCOPE STEP — don't skip this
|
|
54
|
+
|
|
55
|
+
When you authorize the account, Google's guide tells you to open an authorization URL whose scope is **only** `sdm.service`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
…&scope=https://www.googleapis.com/auth/sdm.service
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Do not use that URL.** Use one with the Pub/Sub scope appended, or **device events (motion, doorbell presses, temperature changes) will silently not work**:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
https://nestservices.google.com/partnerconnections/PROJECT-ID/auth?redirect_uri=https://www.google.com&access_type=offline&prompt=consent&client_id=OAUTH2-CLIENT-ID&response_type=code&scope=https://www.googleapis.com/auth/sdm.service+https://www.googleapis.com/auth/pubsub
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Note the `+https://www.googleapis.com/auth/pubsub` on the end. (Replace `PROJECT-ID` with your Device Access project UUID and `OAUTH2-CLIENT-ID` with your client ID.) If you've already generated a refresh token without it, you must re-do this step to get a new one. The symptom of a missing scope is `Plugin initialization failed, there was a failure with event subscription … insufficient authentication scopes`.
|
|
68
|
+
|
|
69
|
+
## Example config
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"platform": "homebridge-google-nest-sdm",
|
|
74
|
+
"clientId": "780816631155-xxxx.apps.googleusercontent.com",
|
|
75
|
+
"clientSecret": "GOCSPX-...",
|
|
76
|
+
"projectId": "4f689e03-....",
|
|
77
|
+
"refreshToken": "1//...",
|
|
78
|
+
"subscriptionId": "projects/my-project-334315/subscriptions/nest-events-sub",
|
|
79
|
+
"gcpProjectId": "my-project-334315",
|
|
80
|
+
"vEncoder": "libx264 -preset ultrafast -tune zerolatency",
|
|
81
|
+
"ffmpegPath": "",
|
|
82
|
+
"snapshotRefresh": true,
|
|
83
|
+
"snapshotRefreshOnAppOpen": true,
|
|
84
|
+
"snapshotRefreshSpacing": 30,
|
|
85
|
+
"snapshotRefreshTtl": 15
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The Homebridge UI config screen is the easiest way to enter these. Only the first five values are required.
|
|
90
|
+
|
|
91
|
+
## Camera snapshots
|
|
92
|
+
|
|
93
|
+
The SDM API has **no on-demand snapshot** for cameras. v2 works around this by caching the **last decoded frame** from streams it opens:
|
|
94
|
+
|
|
95
|
+
- **On motion/person events** a fresh still is grabbed (this is the most useful freshness — a changed tile means something happened).
|
|
96
|
+
- **On app open** (`snapshotRefreshOnAppOpen`) stale tiles are topped up when the Home app requests snapshots.
|
|
97
|
+
|
|
98
|
+
All grabs go through one **serialized queue** with a global spacing (`snapshotRefreshSpacing`, default 30s) and a per-camera freshness window (`snapshotRefreshTtl`, default 15 min), so the feature stays well within Google's rate limits. Tradeoff: lower spacing / shorter TTL = fresher tiles but more SDM calls (closer to HTTP 429). Set `snapshotRefresh: false` to disable entirely (tiles fall back to the placeholder logo). Snapshots are cached under your Homebridge storage directory in `nest-sdm-snapshots/`.
|
|
99
|
+
|
|
100
|
+
## Hardware transcoding
|
|
101
|
+
|
|
102
|
+
`vEncoder` selects the ffmpeg video encoder; `ffmpegPath` selects the ffmpeg **binary**. Together they let you offload transcoding to a GPU:
|
|
103
|
+
|
|
104
|
+
| Platform | `vEncoder` | Notes |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| Intel (Quick Sync) | `h264_qsv` | needs an ffmpeg built with QSV + Intel drivers |
|
|
107
|
+
| macOS | `h264_videotoolbox` | |
|
|
108
|
+
| Raspberry Pi 4 | `h264_v4l2m2m` | |
|
|
109
|
+
| Linux + VAAPI | `h264_vaapi` | |
|
|
110
|
+
| NVIDIA | `h264_nvenc` | needs an NVENC-capable ffmpeg |
|
|
111
|
+
| any (no transcode) | `copy` | lowest CPU, but can't adapt to what HomeKit requests; less reliable |
|
|
112
|
+
|
|
113
|
+
If `vEncoder` names an encoder your ffmpeg doesn't have, streams will fail ("camera not responding"). Point `ffmpegPath` at a build that includes it. Leave `ffmpegPath` blank to use the bundled `ffmpeg-for-homebridge`.
|
|
114
|
+
|
|
115
|
+
## HomeKit Secure Video
|
|
116
|
+
|
|
117
|
+
HKSV is supported. Note how it works: SDM reports motion → the plugin reports it to your hub → the hub requests a stream → the plugin transcodes it → the hub analyzes it for motion and may log a clip. Because of this round-trip, **a motion event may not always produce a timeline clip** (the motion may be over by the time the hub starts analyzing). HKSV transcoding is CPU-heavy; for many cameras you'll want a capable host.
|
|
118
|
+
|
|
119
|
+
## Troubleshooting
|
|
120
|
+
|
|
121
|
+
| Symptom | Likely cause / fix |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `invalid_client` | You mixed up the IDs — `clientId` must be the `…apps.googleusercontent.com` value, not the Device Access UUID. See the table above. |
|
|
124
|
+
| `insufficient authentication scopes` / event subscription failure | Refresh token was generated **without the Pub/Sub scope.** Re-do the authorization with the `+…/auth/pubsub` URL above. Setting `gcpProjectId` can also help. |
|
|
125
|
+
| `SERVICE_DISABLED` / 403 listing devices | The Smart Device Management API isn't enabled on your GCP project. Enable it in *APIs & Services → Library*. |
|
|
126
|
+
| `429` / `RESOURCE_EXHAUSTED` | Google's per-project SDM rate limit, usually from opening many streams at once. v2 handles it gracefully; reduce churn (e.g. don't open many cameras simultaneously) and/or raise `snapshotRefreshSpacing`. You can request a quota increase in the GCP console. |
|
|
127
|
+
| Cameras "not responding" | Check audio/mic is enabled on the camera; ensure ffmpeg exists / your `vEncoder` is supported by your ffmpeg (`ffmpegPath`); disconnect any VPN on the Apple device. |
|
|
128
|
+
| **Docker / Unraid** streams don't work | Running Homebridge in a container breaks ffmpeg input and WebRTC data transfer. **Run Homebridge natively.** |
|
|
129
|
+
| Camera shows as `<null> Camera` | A Google-side glitch; rename it on the HomeKit side after pairing. |
|
|
130
|
+
|
|
131
|
+
## FAQ
|
|
132
|
+
|
|
133
|
+
**Why a fork?** The original is dormant (last release Feb 2024) with open issues. This fork carries fixes and a snapshot feature forward. All credit to potmat for the original.
|
|
134
|
+
|
|
135
|
+
**Do I still have to pay Google $5 for Device Access?** Yes — that's Google's one-time registration fee, unchanged.
|
|
136
|
+
|
|
137
|
+
**Why use the SDM API at all instead of the unofficial-API Nest plugins?** SDM is the documented, push-event API: lower overhead, no fragile reverse-engineered endpoints or account cookies.
|
|
138
|
+
|
|
139
|
+
**Tiles still show the "G" logo.** A tile is a placeholder until the first stream is opened for that camera (then it caches a frame). With `snapshotRefresh` on, it should populate after the first motion/view. Set a shorter `snapshotRefreshTtl` for more frequent updates (at the cost of more SDM calls).
|
|
140
|
+
|
|
141
|
+
## Disclaimer
|
|
142
|
+
|
|
143
|
+
Not affiliated with, provided, endorsed, or supported by Google. For personal, non-commercial use; review the [Google SDM Terms of Service](https://developers.google.com/nest/device-access/tos).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "homebridge-google-nest-sdm",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"headerDisplay": "See the README **Setup** section for how to obtain these values. **IMPORTANT:** when you authorize the account you MUST use the special authorization URL that includes the Pub/Sub scope (`+https://www.googleapis.com/auth/pubsub`), otherwise device events (motion, doorbell, temperature changes) will not work.",
|
|
5
|
+
"singular": "true",
|
|
6
|
+
"schema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"clientId": {
|
|
10
|
+
"title": "OAuth Client ID",
|
|
11
|
+
"description": "From Google Cloud Platform. Ends in '.apps.googleusercontent.com' (e.g. \"780816631155-gbvyo1o7r2pn95qc4ei9d61io4uh48hl.apps.googleusercontent.com\"). NOT the Device Access project UUID.",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"required": true
|
|
14
|
+
},
|
|
15
|
+
"clientSecret": {
|
|
16
|
+
"title": "OAuth Client Secret",
|
|
17
|
+
"description": "From Google Cloud Platform, paired with the Client ID above (starts with 'GOCSPX-').",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"required": true
|
|
20
|
+
},
|
|
21
|
+
"projectId": {
|
|
22
|
+
"title": "Device Access Project ID",
|
|
23
|
+
"description": "The UUID from the Device Access Console (e.g. \"4f689e03-...\"). This is the SDM 'project' and is NOT your Google Cloud project.",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"required": true
|
|
26
|
+
},
|
|
27
|
+
"refreshToken": {
|
|
28
|
+
"title": "Refresh Token",
|
|
29
|
+
"description": "Obtained during account authorization. Must be generated using the authorization URL that includes the Pub/Sub scope (see the header note), or events will not work.",
|
|
30
|
+
"type": "string",
|
|
31
|
+
"required": true
|
|
32
|
+
},
|
|
33
|
+
"subscriptionId": {
|
|
34
|
+
"title": "Pub/Sub Subscription (full path)",
|
|
35
|
+
"description": "Full path form: \"projects/<gcp-project-id>/subscriptions/<subscription-id>\".",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"required": true
|
|
38
|
+
},
|
|
39
|
+
"gcpProjectId": {
|
|
40
|
+
"title": "Google Cloud Project ID",
|
|
41
|
+
"description": "Your Google Cloud project ID (e.g. \"my-project-334315\") — the one the Pub/Sub subscription lives under. Distinct from the Device Access Project ID above.",
|
|
42
|
+
"type": "string",
|
|
43
|
+
"required": false
|
|
44
|
+
},
|
|
45
|
+
"vEncoder": {
|
|
46
|
+
"title": "Video Encoder",
|
|
47
|
+
"description": "ffmpeg video encoder for camera streams. Default is software 'libx264'. For hardware transcoding set e.g. 'h264_qsv' (Intel Quick Sync), 'h264_videotoolbox' (macOS), 'h264_v4l2m2m'/'h264_vaapi' (Linux), or 'h264_nvenc' (NVIDIA) — your ffmpeg build must include the chosen encoder (see ffmpegPath).",
|
|
48
|
+
"type": "string",
|
|
49
|
+
"required": false
|
|
50
|
+
},
|
|
51
|
+
"ffmpegPath": {
|
|
52
|
+
"title": "Custom ffmpeg Path",
|
|
53
|
+
"description": "Absolute path to an ffmpeg binary. Leave blank to use the bundled ffmpeg-for-homebridge, then a system 'ffmpeg'. Set this to point at a build that includes your chosen hardware encoder.",
|
|
54
|
+
"type": "string",
|
|
55
|
+
"required": false
|
|
56
|
+
},
|
|
57
|
+
"snapshotRefresh": {
|
|
58
|
+
"title": "Refresh Camera Snapshots",
|
|
59
|
+
"description": "Periodically grab a still from cameras (on motion, and when you open the Home app) so tiles show a recent image instead of a placeholder. Grabs are serialized and rate-limited to avoid Google SDM 429 throttling.",
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"default": true,
|
|
62
|
+
"required": false
|
|
63
|
+
},
|
|
64
|
+
"snapshotRefreshOnAppOpen": {
|
|
65
|
+
"title": "Refresh On App Open",
|
|
66
|
+
"description": "Also top up stale tiles when the Home app requests snapshots. More current thumbnails, but each grab is a stream start that counts toward Google SDM rate limits. Disable to refresh on motion only.",
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"default": true,
|
|
69
|
+
"required": false,
|
|
70
|
+
"condition": {
|
|
71
|
+
"functionBody": "return model.snapshotRefresh !== false"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"snapshotRefreshSpacing": {
|
|
75
|
+
"title": "Snapshot Refresh Spacing (seconds)",
|
|
76
|
+
"description": "Minimum seconds between snapshot grabs across all cameras. Lower = fresher tiles but closer to the SDM rate limit (HTTP 429).",
|
|
77
|
+
"type": "integer",
|
|
78
|
+
"minimum": 10,
|
|
79
|
+
"default": 30,
|
|
80
|
+
"required": false,
|
|
81
|
+
"condition": {
|
|
82
|
+
"functionBody": "return model.snapshotRefresh !== false"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"snapshotRefreshTtl": {
|
|
86
|
+
"title": "Snapshot Freshness (minutes)",
|
|
87
|
+
"description": "A tile newer than this is considered fresh and won't be re-grabbed.",
|
|
88
|
+
"type": "integer",
|
|
89
|
+
"minimum": 1,
|
|
90
|
+
"default": 15,
|
|
91
|
+
"required": false,
|
|
92
|
+
"condition": {
|
|
93
|
+
"functionBody": "return model.snapshotRefresh !== false"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"showFan": {
|
|
97
|
+
"title": "Show Fan Switch",
|
|
98
|
+
"type": "boolean",
|
|
99
|
+
"required": false
|
|
100
|
+
},
|
|
101
|
+
"fanDuration": {
|
|
102
|
+
"title": "Fan duration (in seconds) when turning on the fan.",
|
|
103
|
+
"type": "integer",
|
|
104
|
+
"minimum": 1,
|
|
105
|
+
"maximum": 43200,
|
|
106
|
+
"default": 900,
|
|
107
|
+
"required": false,
|
|
108
|
+
"condition": {
|
|
109
|
+
"functionBody": "return model.showFan"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Accessory = void 0;
|
|
4
|
+
class Accessory {
|
|
5
|
+
constructor(api, log, platform, accessory, device) {
|
|
6
|
+
this.platform = platform;
|
|
7
|
+
this.log = log;
|
|
8
|
+
this.api = api;
|
|
9
|
+
this.accessory = accessory;
|
|
10
|
+
this.device = device;
|
|
11
|
+
new this.api.hap.Service.AccessoryInformation()
|
|
12
|
+
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Nest');
|
|
13
|
+
}
|
|
14
|
+
async convertToNullable(input) {
|
|
15
|
+
const result = await input;
|
|
16
|
+
if (!result)
|
|
17
|
+
return null;
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.Accessory = Accessory;
|
|
22
|
+
//# sourceMappingURL=Accessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Accessory.js","sourceRoot":"","sources":["../src/Accessory.ts"],"names":[],"mappings":";;;AAGA,MAAsB,SAAS;IAO3B,YACI,GAAQ,EACR,GAAW,EACX,QAAkB,EAClB,SAA4B,EAC5B,MAAS;QACT,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,oBAAoB,EAAE;aAC1C,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;IAC7E,CAAC;IAES,KAAK,CAAC,iBAAiB,CAAI,KAAoC;QACrE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AA3BD,8BA2BC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CameraAccessory = void 0;
|
|
4
|
+
const CameraStreamingDelegate_1 = require("./CameraStreamingDelegate");
|
|
5
|
+
const MotionAccessory_1 = require("./MotionAccessory");
|
|
6
|
+
class CameraAccessory extends MotionAccessory_1.MotionAccessory {
|
|
7
|
+
constructor(api, log, platform, accessory, device) {
|
|
8
|
+
super(api, log, platform, accessory, device);
|
|
9
|
+
this.accessory.on("identify" /* IDENTIFY */, () => {
|
|
10
|
+
this.log.info("%s identified!", this.accessory.displayName);
|
|
11
|
+
});
|
|
12
|
+
this.streamingDelegate = new CameraStreamingDelegate_1.CameraStreamingDelegate(log, api, this.platform, this.device, this.accessory);
|
|
13
|
+
this.accessory.configureController(this.streamingDelegate.getController());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.CameraAccessory = CameraAccessory;
|
|
17
|
+
//# sourceMappingURL=CameraAccessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CameraAccessory.js","sourceRoot":"","sources":["../src/CameraAccessory.ts"],"names":[],"mappings":";;;AAOA,uEAAkE;AAClE,uDAAkD;AAElD,MAAa,eAAgB,SAAQ,iCAAuB;IAGxD,YACI,GAAQ,EACR,GAAW,EACX,QAAkB,EAClB,SAA4B,EAC5B,MAAc;QACd,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QAE7C,IAAI,CAAC,SAAS,CAAC,EAAE,4BAAkC,GAAG,EAAE;YACpD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,iBAAiB,GAAG,IAAI,iDAAuB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3G,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,CAAC,CAAC;IAC/E,CAAC;CACJ;AAlBD,0CAkBC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CameraStreamingDelegate = void 0;
|
|
4
|
+
const StreamingDelegate_1 = require("./StreamingDelegate");
|
|
5
|
+
class CameraStreamingDelegate extends StreamingDelegate_1.StreamingDelegate {
|
|
6
|
+
constructor(log, api, platform, camera, accessory) {
|
|
7
|
+
super(log, api, platform, camera, accessory);
|
|
8
|
+
this.controller = new this.hap.CameraController(this.options);
|
|
9
|
+
}
|
|
10
|
+
getController() {
|
|
11
|
+
return this.controller;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.CameraStreamingDelegate = CameraStreamingDelegate;
|
|
15
|
+
//# sourceMappingURL=CameraStreamingDelegate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CameraStreamingDelegate.js","sourceRoot":"","sources":["../src/CameraStreamingDelegate.ts"],"names":[],"mappings":";;;AAAA,2DAAsD;AAKtD,MAAa,uBAAwB,SAAQ,qCAAmC;IAE5E,YAAY,GAAW,EAAE,GAAQ,EAAE,QAAkB,EAAE,MAAc,EAAE,SAA4B;QAC/F,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;IAED,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;CACJ;AAVD,0DAUC"}
|
package/dist/Config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Config.js","sourceRoot":"","sources":["../src/Config.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DoorbellAccessory = void 0;
|
|
4
|
+
const DoorbellStreamingDelegate_1 = require("./DoorbellStreamingDelegate");
|
|
5
|
+
const MotionAccessory_1 = require("./MotionAccessory");
|
|
6
|
+
class DoorbellAccessory extends MotionAccessory_1.MotionAccessory {
|
|
7
|
+
constructor(api, log, platform, accessory, device) {
|
|
8
|
+
super(api, log, platform, accessory, device);
|
|
9
|
+
this.accessory.on("identify" /* IDENTIFY */, () => {
|
|
10
|
+
this.log.info("%s identified!", this.accessory.displayName);
|
|
11
|
+
});
|
|
12
|
+
this.streamingDelegate = new DoorbellStreamingDelegate_1.DoorbellStreamingDelegate(log, api, this.platform, this.device, this.accessory);
|
|
13
|
+
this.accessory.configureController(this.streamingDelegate.getController());
|
|
14
|
+
this.device.onRing = this.handleRing.bind(this);
|
|
15
|
+
}
|
|
16
|
+
handleRing() {
|
|
17
|
+
this.log.debug('Doorbell ring!', this.accessory.displayName);
|
|
18
|
+
this.streamingDelegate.getController().ringDoorbell();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.DoorbellAccessory = DoorbellAccessory;
|
|
22
|
+
//# sourceMappingURL=DoorbellAccessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DoorbellAccessory.js","sourceRoot":"","sources":["../src/DoorbellAccessory.ts"],"names":[],"mappings":";;;AAOA,2EAAsE;AACtE,uDAAkD;AAElD,MAAa,iBAAkB,SAAQ,iCAAyB;IAG5D,YACI,GAAQ,EACR,GAAW,EACX,QAAkB,EAClB,SAA4B,EAC5B,MAAgB;QAChB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QAE7C,IAAI,CAAC,SAAS,CAAC,EAAE,4BAAkC,GAAG,EAAE;YACpD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,iBAAiB,GAAG,IAAI,qDAAyB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC7G,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAED,UAAU;QACN,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC7D,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,CAAC,YAAY,EAAE,CAAC;IAC1D,CAAC;CACJ;AAxBD,8CAwBC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DoorbellStreamingDelegate = void 0;
|
|
4
|
+
const StreamingDelegate_1 = require("./StreamingDelegate");
|
|
5
|
+
class DoorbellStreamingDelegate extends StreamingDelegate_1.StreamingDelegate {
|
|
6
|
+
constructor(log, api, platform, camera, accessory) {
|
|
7
|
+
super(log, api, platform, camera, accessory);
|
|
8
|
+
this.controller = new this.hap.DoorbellController(this.options);
|
|
9
|
+
}
|
|
10
|
+
getController() {
|
|
11
|
+
return this.controller;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.DoorbellStreamingDelegate = DoorbellStreamingDelegate;
|
|
15
|
+
//# sourceMappingURL=DoorbellStreamingDelegate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DoorbellStreamingDelegate.js","sourceRoot":"","sources":["../src/DoorbellStreamingDelegate.ts"],"names":[],"mappings":";;;AAAA,2DAAsD;AAKtD,MAAa,yBAA0B,SAAQ,qCAAqC;IAEhF,YAAY,GAAW,EAAE,GAAQ,EAAE,QAAkB,EAAE,MAAc,EAAE,SAA4B;QAC/F,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpE,CAAC;IAED,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;CACJ;AAVD,8DAUC"}
|
package/dist/EcoMode.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
module.exports = (homebridge) => {
|
|
3
|
+
var _a;
|
|
4
|
+
return _a = class EcoMode extends homebridge.hap.Characteristic {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('Eco', EcoMode.UUID, {
|
|
7
|
+
format: "bool" /* BOOL */,
|
|
8
|
+
perms: ["pw" /* PAIRED_WRITE */, "pr" /* PAIRED_READ */, "ev" /* NOTIFY */]
|
|
9
|
+
});
|
|
10
|
+
this.value = this.getDefaultValue();
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
_a.UUID = 'f66de49d-792e-44a6-99c8-5e3576328ba1',
|
|
14
|
+
_a;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=EcoMode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EcoMode.js","sourceRoot":"","sources":["../src/EcoMode.ts"],"names":[],"mappings":";AAEA,iBAAS,CAAC,UAAe,EAAE,EAAE;;IACzB,YAAO,MAAM,OAAQ,SAAQ,UAAU,CAAC,GAAG,CAAC,cAAc;YAItD;gBACI,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE;oBACvB,MAAM,mBAAc;oBACpB,KAAK,EAAE,oEAAqD;iBAC/D,CAAC,CAAC;gBACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;YACxC,CAAC;SACJ;QATmB,OAAI,GAAW,sCAAuC;WASzE;AACL,CAAC,CAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.FanAccessory = void 0;
|
|
26
|
+
const Traits = __importStar(require("./sdm/Traits"));
|
|
27
|
+
const Traits_1 = require("./sdm/Traits");
|
|
28
|
+
const Accessory_1 = require("./Accessory");
|
|
29
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
30
|
+
class FanAccessory extends Accessory_1.Accessory {
|
|
31
|
+
constructor(api, log, platform, accessory, device) {
|
|
32
|
+
super(api, log, platform, accessory, device);
|
|
33
|
+
this.config = platform.platformConfig;
|
|
34
|
+
this.accessory.on("identify" /* IDENTIFY */, () => {
|
|
35
|
+
log.info("%s fan identified!", accessory.displayName);
|
|
36
|
+
});
|
|
37
|
+
// create a new Thermostat service
|
|
38
|
+
this.service = accessory.getService(this.api.hap.Service.Fan);
|
|
39
|
+
if (!this.service) {
|
|
40
|
+
this.service = accessory.addService(this.api.hap.Service.Fan);
|
|
41
|
+
}
|
|
42
|
+
this.service.getCharacteristic(this.platform.Characteristic.On)
|
|
43
|
+
.onGet(this.handleOnGet.bind(this))
|
|
44
|
+
.onSet(this.handleOnSet.bind(this));
|
|
45
|
+
this.device.onFanChanged = this.handleFanUpdate.bind(this);
|
|
46
|
+
}
|
|
47
|
+
handleFanUpdate(fan) {
|
|
48
|
+
this.log.debug('Update Fan:' + fan.timerMode, this.accessory.displayName);
|
|
49
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, fan.timerMode === Traits_1.FanTimerModeType.ON);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Handle requests to set the "On" characteristic
|
|
53
|
+
*/
|
|
54
|
+
async handleOnSet(value) {
|
|
55
|
+
this.log.debug('Triggered SET Fan', this.accessory.displayName);
|
|
56
|
+
if (!lodash_1.default.isBoolean(value))
|
|
57
|
+
throw new Error(`Cannot set "${value}" as fan state.`);
|
|
58
|
+
if (this.config.fanDuration && (this.config.fanDuration < 1 || this.config.fanDuration > 43200))
|
|
59
|
+
throw new Error(`Cannot set "${this.config.fanDuration}" as fan duration.`);
|
|
60
|
+
await this.device.setFan(value ? Traits.FanTimerModeType.ON : Traits_1.FanTimerModeType.OFF, this.config.fanDuration);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Handle requests to get the current value of the "On" characteristic
|
|
64
|
+
*/
|
|
65
|
+
async handleOnGet() {
|
|
66
|
+
this.log.debug('Triggered GET Fan On', this.accessory.displayName);
|
|
67
|
+
const fan = await this.device.getFan();
|
|
68
|
+
switch (fan === null || fan === void 0 ? void 0 : fan.timerMode) {
|
|
69
|
+
case Traits_1.FanTimerModeType.ON:
|
|
70
|
+
return true;
|
|
71
|
+
default:
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.FanAccessory = FanAccessory;
|
|
77
|
+
//# sourceMappingURL=FanAccessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FanAccessory.js","sourceRoot":"","sources":["../src/FanAccessory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FfmpegProcess = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const stream_1 = require("stream");
|
|
6
|
+
const util_1 = require("./util");
|
|
7
|
+
class FfmpegProcess {
|
|
8
|
+
constructor(cameraName, sessionId, ffmpegArgs, stdin, log, debug, delegate, callback, ffmpegPath, extraArgs) {
|
|
9
|
+
const pathToFfmpeg = (0, util_1.resolveFfmpegPath)(ffmpegPath);
|
|
10
|
+
log.debug(`Stream command: ${pathToFfmpeg} ${ffmpegArgs} ${stdin}`, cameraName);
|
|
11
|
+
// extraArgs are passed as discrete tokens (not whitespace-split) so values that
|
|
12
|
+
// may contain spaces — e.g. a snapshot cache path — survive intact.
|
|
13
|
+
let started = false;
|
|
14
|
+
this.process = (0, child_process_1.spawn)(pathToFfmpeg, ffmpegArgs.split(/\s+/).concat(extraArgs !== null && extraArgs !== void 0 ? extraArgs : []), { env: process.env, stdio: 'pipe' });
|
|
15
|
+
if (!this.process.stdin && stdin) {
|
|
16
|
+
log.error('FFmpegProcess failed to start stream: input to ffmpeg was provided as stdin, but the process does not support stdin.', cameraName);
|
|
17
|
+
delegate.stopStream(sessionId);
|
|
18
|
+
}
|
|
19
|
+
if (this.process.stdin) {
|
|
20
|
+
this.process.stdin.on('error', (error) => {
|
|
21
|
+
if (!error.message.includes('EPIPE')) {
|
|
22
|
+
log.error(error.message, cameraName);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (stdin) {
|
|
26
|
+
const sdpStream = this.convertStringToStream(stdin);
|
|
27
|
+
sdpStream.resume();
|
|
28
|
+
sdpStream.pipe(this.process.stdin);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (this.process.stderr) {
|
|
32
|
+
this.process.stderr.on('data', (data) => {
|
|
33
|
+
if (!started) {
|
|
34
|
+
started = true;
|
|
35
|
+
if (callback) {
|
|
36
|
+
callback();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (debug) {
|
|
40
|
+
data.toString().split(/\n/).forEach((line) => {
|
|
41
|
+
log.debug(line, cameraName);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
this.process.on('error', (error) => {
|
|
47
|
+
log.error('Failed to start stream: ' + error.message, cameraName);
|
|
48
|
+
if (callback) {
|
|
49
|
+
callback(new Error('FFmpeg process creation failed'));
|
|
50
|
+
}
|
|
51
|
+
delegate.stopStream(sessionId);
|
|
52
|
+
});
|
|
53
|
+
this.process.on('exit', (code, signal) => {
|
|
54
|
+
const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;
|
|
55
|
+
if (code == null || code === 255) {
|
|
56
|
+
if (this.process.killed) {
|
|
57
|
+
log.debug(message + ' (Expected)', cameraName);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
log.error(message + ' (Unexpected)', cameraName);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
log.error(message + ' (Error)', cameraName);
|
|
65
|
+
delegate.stopStream(sessionId);
|
|
66
|
+
if (!started && callback) {
|
|
67
|
+
callback(new Error(message));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
delegate.getController().forceStopStreamingSession(sessionId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
stop() {
|
|
76
|
+
this.process.kill('SIGKILL');
|
|
77
|
+
}
|
|
78
|
+
getStdin() {
|
|
79
|
+
return this.process.stdin;
|
|
80
|
+
}
|
|
81
|
+
convertStringToStream(stringToConvert) {
|
|
82
|
+
const stream = new stream_1.Readable();
|
|
83
|
+
stream._read = () => { };
|
|
84
|
+
stream.push(stringToConvert);
|
|
85
|
+
stream.push(null);
|
|
86
|
+
return stream;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.FfmpegProcess = FfmpegProcess;
|
|
90
|
+
//# sourceMappingURL=FfMpegProcess.js.map
|