scrypted-kasa 1.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.
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: npm
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: github-actions
8
+ directory: "/"
9
+ schedule:
10
+ interval: weekly
11
+ groups:
12
+ ci:
13
+ patterns:
14
+ - "*"
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ # Cancel in-progress runs for the same PR/branch when a new commit is pushed.
10
+ concurrency:
11
+ group: ci-${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ check:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
19
+
20
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
21
+ with:
22
+ node-version-file: '.nvmrc'
23
+ cache: 'npm'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Prettier check
29
+ run: npm run fmt:check
30
+
31
+ - name: ESLint
32
+ run: npm run lint
33
+
34
+ - name: Build (scrypted-webpack)
35
+ run: npm run build
@@ -0,0 +1,52 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ # Trigger on semver tags like v1.0.7. Push the tag with `git tag v1.0.7 && git push --tags`
6
+ # after bumping the version in package.json.
7
+ tags: ['v*']
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ # id-token is required for npm publish provenance — provenance signs the package
15
+ # via the GitHub OIDC token so consumers can verify it was built by this workflow.
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
19
+
20
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
21
+ with:
22
+ node-version-file: '.nvmrc'
23
+ cache: 'npm'
24
+ # Configures npm with the registry URL so NODE_AUTH_TOKEN is honored at publish.
25
+ registry-url: 'https://registry.npmjs.org'
26
+
27
+ - name: Verify package.json version matches git tag
28
+ run: |
29
+ PKG_VERSION=$(node -p "require('./package.json').version")
30
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
31
+ if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
32
+ echo "::error::package.json version ($PKG_VERSION) does not match git tag ($TAG_VERSION)"
33
+ exit 1
34
+ fi
35
+ echo "Publishing scrypted-kasa@$PKG_VERSION"
36
+
37
+ - name: Install dependencies
38
+ run: npm ci
39
+
40
+ - name: Prettier check
41
+ run: npm run fmt:check
42
+
43
+ - name: ESLint
44
+ run: npm run lint
45
+
46
+ - name: Build (scrypted-webpack)
47
+ run: npm run build
48
+
49
+ - name: Publish to npm
50
+ run: npm publish --access public --provenance
51
+ env:
52
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v24
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ out/
3
+ build/
4
+ dist/
5
+ *.log
6
+ package-lock.json
@@ -0,0 +1,11 @@
1
+ {
2
+ "tabWidth": 4,
3
+ "useTabs": false,
4
+ "singleQuote": true,
5
+ "semi": true,
6
+ "trailingComma": "all",
7
+ "printWidth": 120,
8
+ "arrowParens": "avoid",
9
+ "bracketSpacing": true,
10
+ "endOfLine": "lf"
11
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergey Vilgelm
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,184 @@
1
+ # scrypted-kasa
2
+
3
+ [![npm version](https://img.shields.io/npm/v/scrypted-kasa.svg)](https://www.npmjs.com/package/scrypted-kasa)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ Adds support for the TP-Link Kasa device family to Scrypted: cameras, plugs/outlets,
7
+ switches/dimmers, and bulbs. Discovery is unified — one UDP/9999 sweep finds everything
8
+ and the plugin routes each device to the right Scrypted interfaces.
9
+
10
+ ## Installation
11
+
12
+ In Scrypted:
13
+
14
+ 1. Open the **Plugins** page.
15
+ 2. Choose **Install** and enter `scrypted-kasa`.
16
+ 3. The plugin appears as **Kasa Plugin** in the device list — see [Setup](#setup) below.
17
+
18
+ ## Cameras
19
+
20
+ - Reads the camera's proprietary multipart stream (HTTPS port 19443, `/https/stream/mixed`)
21
+ and re-streams H.264 video + G.711 µ-law audio over local RTSP for Scrypted, HomeKit, the
22
+ NVR, etc.
23
+ - Streams talk audio back to the camera's speaker via the camera's separate uplink endpoint
24
+ (HTTPS port 18443, `/https/speaker/audio/g711block`) — usable from HomeKit and any other
25
+ Scrypted client that supports two-way audio.
26
+ - Exposes the camera's spotlight (when present, e.g. KC420WS) as an `OnOff` child light
27
+ device in the same room as the camera. Driven by the LINKIE2 control protocol on port
28
+ 10443.
29
+ - Exposes the camera's siren (when present) as an `OnOff` child switch device, also in
30
+ the camera's room. Triggering it turns the siren on; the camera auto-stops after the
31
+ duration configured in the Kasa app (default 30 s).
32
+ - Exposes the camera's **status LED** as the camera's own `OnOff` interface, so HomeKit's
33
+ per-camera "Link Status Indicator" toggle drives the LED.
34
+
35
+ ## Plugs / Switches / Dimmers / Bulbs (legacy IOT protocol)
36
+
37
+ Each device class is its own implementation, descended from a shared `KasaIotDevice`
38
+ base that holds the relay protocol:
39
+
40
+ - **`KasaPlug`** — plain plugs/outlets (HS100/HS103/HS105/HS107/HS110/KP100/...). `OnOff`,
41
+ `ScryptedDeviceType.Outlet`.
42
+ - **`KasaSwitch`** — plain wall switches (HS200/HS210/KS200/...). `OnOff`,
43
+ `ScryptedDeviceType.Switch`.
44
+ - **`KasaDimmer`** — dimmer plugs and switches (HS220 plug, KS230 3-way switch). `OnOff` +
45
+ `Brightness`, `ScryptedDeviceType.Light` (dimmable devices are exposed as lights since
46
+ they're almost always wired to a light fixture, matching the Kasa app's UX).
47
+ - **`KasaBulb`** — smart bulbs (LB1xx, KL1xx). `OnOff` + `Brightness`, plus
48
+ `ColorSettingHsv` for color bulbs and `ColorSettingTemperature` for variable-temperature
49
+ bulbs.
50
+
51
+ Multi-outlet plug strips (HS300, KP303) aren't modeled yet — discovery skips them.
52
+
53
+ Stream protocol described in [go2rtc's `pkg/kasa`](https://github.com/AlexxIT/go2rtc/tree/master/pkg/kasa);
54
+ camera control protocols (LINKIE2, talk endpoint, status LED) modeled after the official
55
+ Kasa iOS app's network traffic. The plug/bulb/switch protocol is the well-documented
56
+ legacy "smarthome" TCP/9999 wire format.
57
+
58
+ Tested camera models: KD110, KC200, KC401, KC420WS, EC71.
59
+
60
+ ## Setup
61
+
62
+ ### Auto-discovery (recommended)
63
+
64
+ 1. Install the plugin.
65
+ 2. Open the plugin's page and click **Discover Devices**. Discovery runs only when you
66
+ click the button — there are no background scans.
67
+ 3. Every LAN-visible Kasa device appears in the list — cameras, plugs, switches, dimmers,
68
+ and bulbs are all discovered together.
69
+ 4. For each device you want to adopt, fill in the adoption form and click adopt:
70
+ - **Name** — pre-filled with the device's alias or model; edit to taste.
71
+ - **Room** — optional, picked from a dropdown of rooms already in use by other
72
+ Scrypted devices, or type a new one.
73
+ - **Username** / **Password** *(cameras only)* — your Kasa account email and password.
74
+ After the first camera is configured these are pre-populated from any other Kasa
75
+ camera in Scrypted.
76
+
77
+ Adopted devices get their IP, port, name, model, MAC, serial number, and firmware version
78
+ populated automatically. The manufacturer is reported as `TP-Link Kasa`.
79
+
80
+ Discovery sends a single UDP/9999 burst on each connected /24:
81
+
82
+ - A **broadcast** of the IOT.SMARTHOME `get_sysinfo` query.
83
+ - A **paced unicast** of the same query at every IP on the subnet (~3 ms between sends).
84
+ Newer firmwares (e.g. KC420WS cameras) drop broadcast probes but still answer when the
85
+ packet is addressed directly. Pacing keeps the kernel/network from coalescing or
86
+ dropping the burst.
87
+
88
+ If a device is on a different VLAN/broadcast domain or a non-/24 subnet, use the manual
89
+ setup below.
90
+
91
+ ### Manual setup
92
+
93
+ 1. Install the plugin.
94
+ 2. Use **Add Device** to create a new device. The Add form takes:
95
+ - **Type** — Camera, Plug, Switch, Dimmer, or Bulb.
96
+ - **Name** — required.
97
+ - **Room** — optional, with the same dropdown of existing rooms as the discovery flow.
98
+ 3. After creation, open the device's settings and fill in:
99
+ - **IP Address** of the device on your LAN
100
+ - **Port** (default 19443 for cameras, 9999 for everything else)
101
+ - For cameras: **Username** + **Password** (your Kasa account email + password)
102
+
103
+ ## How it works
104
+
105
+ ### Receive (camera → Scrypted)
106
+
107
+ - The plugin opens a single HTTPS connection to the camera on port 19443 and parses the
108
+ `multipart/x-mixed-replace` body, splitting it into video parts (`video/x-h264`,
109
+ annex-b) and audio parts (`audio/g711u`).
110
+ - It scans the first H.264 frames for SPS (NAL 7) and PPS (NAL 8) and inlines them into
111
+ the locally served SDP as `sprop-parameter-sets` + `profile-level-id`. This lets short
112
+ -timeout consumers (HomeKit, browser players) pick up the codec immediately.
113
+ - It then runs ffmpeg in codec-copy mode, re-packetizing the raw H.264 and G.711 µ-law
114
+ streams as RTP into a local RTSP server that Scrypted reads from.
115
+
116
+ ### Talk (Scrypted → camera speaker)
117
+
118
+ - When a client engages two-way audio, the plugin opens a long-lived chunked POST to
119
+ `https://<ip>:18443/https/speaker/audio/g711block` and ffmpeg-transcodes the client's
120
+ audio to 8 kHz mono G.711 µ-law.
121
+ - Each 20 ms (160 byte) audio block is wrapped in a `multipart/x-mixed-replace` part with
122
+ `Content-Type: audio/g711u` and streamed to the camera. Empty `audio/heartbeat` parts
123
+ are sent every 3 s during silence to keep the connection alive — same pattern the Kasa
124
+ app uses.
125
+
126
+ ### Spotlight / control plane
127
+
128
+ - The Kasa app talks to the camera's "LINKIE2" RPC on port 10443 for non-streaming
129
+ features. Wire format: HTTPS POST `/data/LINKIE2.json`, body `application/x-www-form-
130
+ urlencoded` with a single `content=<base64(xor_ab(json))>` field. The XOR-AB autokey
131
+ cipher is the same one used by Kasa's UDP/9999 discovery protocol.
132
+ - On adoption (and whenever credentials change) the plugin probes three LINKIE2 endpoints
133
+ serially:
134
+ - `smartlife.cam.ipcamera.dayNight.get_force_lamp_state` — if it returns `on`/`off`, a
135
+ child `OnOff` light device named "<camera> Spotlight" is registered in the camera's
136
+ room.
137
+ - `smartlife.cam.ipcamera.siren.get_state` — same pattern; produces a "<camera> Siren"
138
+ switch child device.
139
+ - `smartlife.cam.ipcamera.led.get_status` — drives the camera's own `OnOff` so HomeKit
140
+ can bind its `CameraOperatingModeIndicator` (a.k.a. "Status Light") characteristic to
141
+ it. Enable **Link Status Indicator** in the HomeKit plugin's per-camera settings to
142
+ activate the binding.
143
+ - Toggling any of the three calls the matching `set_*` method with `{"value": "on"|"off"}`.
144
+
145
+ ## Notes / limitations
146
+
147
+ - **Authentication** uses the cloud account password. All three endpoints take Basic auth
148
+ with the plain Kasa email as the username; the password format differs by endpoint —
149
+ receive uses base64(plaintext), talk and LINKIE2 use md5_hex(plaintext). The plugin
150
+ uses each in the right place.
151
+ - LINKIE2 requests must include a `User-Agent: Kasa/...` header and the `Authorization`
152
+ header on the very first request — the camera silently drops requests that don't look
153
+ enough like the official app.
154
+ - The camera presents a self-signed TLS certificate; certificate verification is disabled.
155
+ - Auto-discovery sweeps the local /24 only. Larger subnets (e.g. /23) are skipped to
156
+ avoid flooding.
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ git clone https://github.com/sv-tools/scrypted-kasa-plugin.git
162
+ cd scrypted-kasa-plugin
163
+ npm install
164
+ ```
165
+
166
+ Common scripts:
167
+
168
+ | Command | Purpose |
169
+ |---|---|
170
+ | `npm run build` | Webpack bundle into `out/main.nodejs.js` (+ `plugin.zip`) |
171
+ | `npm run fmt` | Format `src/**/*.ts` with prettier |
172
+ | `npm run fmt:check` | Verify formatting — what CI runs |
173
+ | `npm run lint` | ESLint |
174
+ | `npm run lint:fix` | ESLint with auto-fix |
175
+ | `npm run scrypted-deploy <host>` | Build and deploy to a Scrypted server |
176
+
177
+ CI on every PR runs `fmt:check`, `lint`, and `build`. Releases publish to npm
178
+ automatically when a `v*` tag is pushed (e.g. `git tag v1.2.3 && git push --tags`).
179
+ The publish workflow signs the package with [npm provenance](https://docs.npmjs.com/generating-provenance-statements)
180
+ via GitHub OIDC.
181
+
182
+ ## License
183
+
184
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,33 @@
1
+ import js from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+
4
+ // We deliberately don't use `eslint-config-prettier`: typescript-eslint's `recommended`
5
+ // preset focuses on code-quality rules, not style. Prettier already owns formatting via
6
+ // `npm run fmt` / `fmt:check`, and there's nothing in the rule set below that fights it.
7
+ // Skipping the dep also sidesteps the known eslint-config-prettier supply-chain compromise.
8
+ export default tseslint.config(
9
+ {
10
+ ignores: ['node_modules/', 'out/', 'build/', 'dist/'],
11
+ },
12
+ js.configs.recommended,
13
+ ...tseslint.configs.recommended,
14
+ {
15
+ rules: {
16
+ // Allow `any` — used intentionally for kasa sysinfo (untyped JSON shape varies
17
+ // per device family) and for narrow casts where TypeScript can't see through
18
+ // the runtime tag (e.g. `kasaClass`).
19
+ '@typescript-eslint/no-explicit-any': 'off',
20
+
21
+ // Treat `_`-prefixed args/vars as intentionally unused (matches the convention
22
+ // we already use for ignored handler params like `_options`, `_id`, `_nativeId`).
23
+ '@typescript-eslint/no-unused-vars': [
24
+ 'warn',
25
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
26
+ ],
27
+
28
+ // Empty catch blocks are a deliberate teardown idiom in this codebase: e.g.
29
+ // `try { socket.close(); } catch {}` — best-effort, ignore failure. Don't nag.
30
+ 'no-empty': ['warn', { allowEmptyCatch: true }],
31
+ },
32
+ },
33
+ );
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "scrypted-kasa",
3
+ "version": "1.0.0",
4
+ "description": "TP-Link Kasa plugin for Scrypted — cameras (video, two-way audio, spotlight, siren, status LED), plugs/outlets, wall switches, dimmers, and smart bulbs (on/off, brightness, color, color temperature).",
5
+ "author": "Sergey Vilgelm <sergey@vilgelm.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sv-tools/scrypted-kasa-plugin.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/sv-tools/scrypted-kasa-plugin/issues"
13
+ },
14
+ "homepage": "https://github.com/sv-tools/scrypted-kasa-plugin#readme",
15
+ "scripts": {
16
+ "scrypted-setup-project": "scrypted-setup-project",
17
+ "prescrypted-setup-project": "scrypted-package-json",
18
+ "build": "scrypted-webpack",
19
+ "prepublishOnly": "NODE_ENV=production scrypted-webpack",
20
+ "prescrypted-vscode-launch": "scrypted-webpack",
21
+ "scrypted-vscode-launch": "scrypted-deploy-debug",
22
+ "scrypted-deploy-debug": "scrypted-deploy-debug",
23
+ "scrypted-debug": "scrypted-debug",
24
+ "scrypted-deploy": "scrypted-deploy",
25
+ "scrypted-readme": "scrypted-readme",
26
+ "scrypted-package-json": "scrypted-package-json",
27
+ "fmt": "prettier --write \"src/**/*.ts\"",
28
+ "fmt:check": "prettier --check \"src/**/*.ts\"",
29
+ "lint": "eslint src",
30
+ "lint:fix": "eslint src --fix"
31
+ },
32
+ "keywords": [
33
+ "scrypted",
34
+ "scrypted-plugin",
35
+ "plugin",
36
+ "kasa",
37
+ "tp-link",
38
+ "tplink",
39
+ "smarthome",
40
+ "smart-home",
41
+ "homekit",
42
+ "camera",
43
+ "intercom",
44
+ "two-way-audio",
45
+ "switch",
46
+ "outlet",
47
+ "plug",
48
+ "dimmer",
49
+ "bulb",
50
+ "light",
51
+ "lightbulb"
52
+ ],
53
+ "scrypted": {
54
+ "name": "Kasa Plugin",
55
+ "type": "DeviceProvider",
56
+ "interfaces": [
57
+ "ScryptedSystemDevice",
58
+ "ScryptedDeviceCreator",
59
+ "DeviceProvider",
60
+ "DeviceCreator",
61
+ "DeviceDiscovery"
62
+ ],
63
+ "pluginDependencies": [
64
+ "@scrypted/prebuffer-mixin"
65
+ ]
66
+ },
67
+ "devDependencies": {
68
+ "@eslint/js": "^10.0.1",
69
+ "@scrypted/sdk": "^0.5.59",
70
+ "@types/node": "^24.12.2",
71
+ "eslint": "^10.2.1",
72
+ "prettier": "^3.8.3",
73
+ "typescript-eslint": "^8.59.1"
74
+ }
75
+ }