homebridge-tekmar 0.1.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/CLAUDE.md ADDED
@@ -0,0 +1,50 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ Homebridge dynamic platform plugin for Tekmar tN4 WiFi thermostats (models 561-564) controlled via the Watts Home cloud API.
6
+
7
+ ## Architecture
8
+
9
+ - **TypeScript** dynamic platform plugin (ES2022, nodenext modules)
10
+ - **No runtime dependencies** — uses Node 20+ native `fetch`
11
+ - **Polling architecture** — platform polls API on interval, pushes updates via `updateCharacteristic()`. GET handlers return cached data instantly to prevent HomeKit "Not Responding" errors.
12
+ - **Heat-only** — Tekmar 561 is heating-only. TargetHeatingCoolingState: OFF, HEAT, AUTO (schedule).
13
+
14
+ ## Key Files
15
+
16
+ - `src/index.ts` — Entry point, registers platform
17
+ - `src/settings.ts` — API constants (Azure B2C URLs, client IDs)
18
+ - `src/platform.ts` — DynamicPlatformPlugin: discovery, polling, accessory lifecycle
19
+ - `src/platformAccessory.ts` — HomeKit Thermostat service characteristics
20
+ - `src/api/types.ts` — TypeScript interfaces for API responses
21
+ - `src/api/wattsApi.ts` — API client: Azure B2C auth, REST calls, temp conversion
22
+
23
+ ## API Details
24
+
25
+ - **API Base**: `https://home.watts.com/api`
26
+ - **Auth**: Azure AD B2C PKCE OAuth2 at `login.watts.io`
27
+ - **Temperatures**: Plain integers in Fahrenheit from the API; HomeKit uses Celsius internally
28
+ - **Modes**: String values `"Heat"` / `"Off"` (not integer codes)
29
+ - **Control**: `PATCH /api/Device/{id}` with `{"Settings":{...}}`
30
+
31
+ ## Build & Test
32
+
33
+ ```bash
34
+ npm install # Install dev dependencies
35
+ npm run build # Compile TypeScript to dist/
36
+ npm run watch # Compile with file watcher
37
+ npm run dev # nodemon auto-restart
38
+ ```
39
+
40
+ ### Standalone auth test:
41
+ ```bash
42
+ WATTS_USERNAME="you@email.com" WATTS_PASSWORD="yourpass" npx tsx test-auth.ts
43
+ ```
44
+
45
+ ## Important Notes
46
+
47
+ - Azure B2C `tx` query parameter must have literal `StateProperties=` (not URL-encoded `%3D`)
48
+ - B2C SelfAsserted endpoint returns HTTP 200 with error in JSON body `{"status":"400",...}`
49
+ - Auth flow requires manual redirect following with cookie jar accumulation
50
+ - `URLSearchParams` over-encodes some characters for B2C — query strings for SelfAsserted/confirmed are built manually
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Schmelkin
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,176 @@
1
+ # homebridge-tekmar
2
+
3
+ [![npm version](https://badge.fury.io/js/homebridge-tekmar.svg)](https://badge.fury.io/js/homebridge-tekmar)
4
+ [![npm downloads](https://img.shields.io/npm/dt/homebridge-tekmar.svg)](https://www.npmjs.com/package/homebridge-tekmar)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A [Homebridge](https://homebridge.io) plugin for [Tekmar](https://www.watts.com/products/heating-cooling-comfort/smart-home/thermostats) tN4 WiFi thermostats (561/562/563/564). Control your Tekmar radiant floor heating thermostat through Apple HomeKit via the Watts Home cloud API.
8
+
9
+ ## Features
10
+
11
+ - **Current Temperature** — Room, floor, and outdoor sensor readings
12
+ - **Target Temperature** — Set your heating setpoint from the Home app or Siri
13
+ - **Mode Control** — Off, Heat, and Auto (scheduled program) modes
14
+ - **Automatic Polling** — Keeps HomeKit in sync with your thermostat state
15
+ - **Easy Configuration** — Set up through Homebridge Config UI X
16
+ - **No Runtime Dependencies** — Uses Node.js native `fetch`, no extra packages needed
17
+ - **Child Bridge Support** — Run as an isolated child bridge for stability
18
+
19
+ ## Supported Hardware
20
+
21
+ | Model | Name | Type |
22
+ |-------|------|------|
23
+ | 561 | tN4 Smart Thermostat | WiFi radiant floor heating |
24
+ | 562 | tN4 Smart Thermostat | WiFi radiant floor heating |
25
+ | 563 | tN4 Smart Thermostat | WiFi radiant floor heating |
26
+ | 564 | tN4 Smart Thermostat | WiFi radiant floor heating |
27
+
28
+ These thermostats are controlled via the **Watts Home** mobile app ([iOS](https://apps.apple.com/app/watts-home/id1452498498) / [Android](https://play.google.com/store/apps/details?id=com.watts.mobileapp)). You must have a Watts Home account with your thermostat(s) configured before using this plugin.
29
+
30
+ ## Installation
31
+
32
+ ### Option 1: Via Homebridge Config UI X (Recommended)
33
+
34
+ 1. Search for `homebridge-tekmar` in the Homebridge Config UI X plugins tab
35
+ 2. Click **Install**
36
+ 3. Configure with your Watts Home account credentials
37
+
38
+ ### Option 2: Via npm
39
+
40
+ ```bash
41
+ npm install -g homebridge-tekmar
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ Configure the plugin through the Homebridge Config UI X interface, or manually edit your `config.json`:
47
+
48
+ ```json
49
+ {
50
+ "platforms": [
51
+ {
52
+ "platform": "TekmarThermostat",
53
+ "name": "Tekmar Thermostat",
54
+ "username": "your@email.com",
55
+ "password": "your-password"
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ ### Configuration Options
62
+
63
+ | Option | Default | Description |
64
+ |--------|---------|-------------|
65
+ | `username` | *required* | Your Watts Home app email address |
66
+ | `password` | *required* | Your Watts Home app password |
67
+ | `pollingInterval` | `60` | Polling interval in seconds (10-300) |
68
+ | `temperatureUnit` | `fahrenheit` | Display unit: `fahrenheit` or `celsius` |
69
+
70
+ ## How It Works
71
+
72
+ 1. Authenticates with the Watts Home cloud API (Azure AD B2C OAuth2)
73
+ 2. Discovers all Tekmar thermostats across your locations
74
+ 3. Exposes each thermostat as a HomeKit Thermostat accessory
75
+ 4. Polls the API at a configurable interval to keep state in sync
76
+ 5. SET commands (temperature, mode) are sent immediately to the API
77
+
78
+ ### HomeKit Mode Mapping
79
+
80
+ The Tekmar 561 is a **heat-only** thermostat. HomeKit modes are mapped as:
81
+
82
+ | HomeKit Mode | Tekmar Behavior |
83
+ |-------------|-----------------|
84
+ | **Off** | Thermostat off |
85
+ | **Heat** | Manual heating to target temperature |
86
+ | **Auto** | Scheduled program mode enabled |
87
+
88
+ > **Note:** Cool mode is not available — the Tekmar tN4 series does not support cooling.
89
+
90
+ ## Supported Features
91
+
92
+ - Set and read target temperature
93
+ - Read current room temperature
94
+ - Switch between Off / Heat / Auto modes
95
+ - Current heating state (actively heating or idle)
96
+ - Temperature display unit preference
97
+ - Multiple thermostats across multiple locations
98
+
99
+ ## Known Limitations
100
+
101
+ - **Cloud API only** — Requires internet connectivity (no local control)
102
+ - **Heat only** — No cooling support (hardware limitation)
103
+ - **No humidity sensor** — The thermostat does not report humidity
104
+ - **Floor/outdoor temps** — Floor and outdoor sensor readings are fetched but not yet exposed as separate HomeKit sensors
105
+
106
+ ## Development
107
+
108
+ ### Local Development
109
+
110
+ ```bash
111
+ # Clone the repository
112
+ git clone https://github.com/apumapho/homebridge-tekmar.git
113
+ cd homebridge-tekmar
114
+
115
+ # Install dependencies
116
+ npm install
117
+
118
+ # Build
119
+ npm run build
120
+
121
+ # Link for local testing
122
+ npm link
123
+
124
+ # Run Homebridge in debug mode
125
+ homebridge -D
126
+ ```
127
+
128
+ ### Standalone API Test
129
+
130
+ ```bash
131
+ WATTS_USERNAME="you@email.com" WATTS_PASSWORD="yourpass" npx tsx test-auth.ts
132
+ ```
133
+
134
+ ## Troubleshooting
135
+
136
+ ### Thermostat not appearing in HomeKit
137
+
138
+ 1. Check Homebridge logs for authentication errors
139
+ 2. Verify your Watts Home credentials are correct
140
+ 3. Ensure your thermostat is set up in the Watts Home app
141
+ 4. Try restarting Homebridge
142
+
143
+ ### "Not Responding" in Home app
144
+
145
+ - Check your internet connection
146
+ - Verify the Watts Home cloud service is operational
147
+ - Increase `pollingInterval` if you're hitting rate limits
148
+
149
+ ### Authentication failures
150
+
151
+ - Ensure your email and password match what you use in the Watts Home app
152
+ - Check if your account has been locked (too many failed attempts)
153
+ - The plugin uses the same authentication flow as the mobile app
154
+
155
+ ## About Tekmar / Watts
156
+
157
+ [Tekmar](https://www.watts.com/brands/tekmar) is a Watts brand specializing in radiant heating controls. The tN4 WiFi thermostat series connects to the Watts Home cloud platform for remote control. This plugin uses the Watts Home cloud API to integrate with HomeKit.
158
+
159
+ ## License
160
+
161
+ MIT License - see [LICENSE](LICENSE) file for details.
162
+
163
+ ## Contributing
164
+
165
+ Contributions are welcome! Please feel free to submit a Pull Request.
166
+
167
+ ## Acknowledgments
168
+
169
+ - Thanks to the [Homebridge](https://homebridge.io) team for the excellent platform
170
+ - Built with [Claude Code](https://claude.com/claude-code)
171
+
172
+ ## Support
173
+
174
+ - [Report Issues](https://github.com/apumapho/homebridge-tekmar/issues)
175
+ - [Homebridge Discord](https://discord.gg/homebridge)
176
+ - [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki)
@@ -0,0 +1,66 @@
1
+ {
2
+ "pluginAlias": "TekmarThermostat",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Homebridge plugin for [Tekmar tN4 WiFi thermostats](https://www.watts.com/products/heating-cooling-comfort/smart-home/thermostats) (561/562/563/564) via the Watts Home cloud API.",
6
+ "footerDisplay": "For help, visit the [GitHub repository](https://github.com/apumapho/homebridge-tekmar).",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Name",
12
+ "type": "string",
13
+ "default": "Tekmar Thermostat",
14
+ "required": true
15
+ },
16
+ "username": {
17
+ "title": "Email",
18
+ "type": "string",
19
+ "format": "email",
20
+ "description": "Your Watts Home app email address.",
21
+ "required": true
22
+ },
23
+ "password": {
24
+ "title": "Password",
25
+ "type": "string",
26
+ "description": "Your Watts Home app password.",
27
+ "required": true
28
+ },
29
+ "pollingInterval": {
30
+ "title": "Polling Interval (seconds)",
31
+ "type": "integer",
32
+ "default": 60,
33
+ "minimum": 10,
34
+ "maximum": 300,
35
+ "description": "How often to poll the Watts Home API for thermostat state updates."
36
+ },
37
+ "temperatureUnit": {
38
+ "title": "Temperature Display Unit",
39
+ "type": "string",
40
+ "default": "fahrenheit",
41
+ "oneOf": [
42
+ { "title": "Fahrenheit", "enum": ["fahrenheit"] },
43
+ { "title": "Celsius", "enum": ["celsius"] }
44
+ ],
45
+ "description": "Preferred temperature display unit in the Home app."
46
+ }
47
+ }
48
+ },
49
+ "layout": [
50
+ "name",
51
+ "username",
52
+ {
53
+ "key": "password",
54
+ "type": "password"
55
+ },
56
+ {
57
+ "type": "fieldset",
58
+ "title": "Advanced Settings",
59
+ "expandable": true,
60
+ "items": [
61
+ "pollingInterval",
62
+ "temperatureUnit"
63
+ ]
64
+ }
65
+ ]
66
+ }
@@ -0,0 +1,144 @@
1
+ import type { PlatformConfig } from 'homebridge';
2
+ export interface TekmarPlatformConfig extends PlatformConfig {
3
+ username: string;
4
+ password: string;
5
+ pollingInterval?: number;
6
+ temperatureUnit?: 'fahrenheit' | 'celsius';
7
+ }
8
+ export interface WattsApiResponse<T> {
9
+ errorNumber: number;
10
+ errorMessage: string | null;
11
+ body: T;
12
+ }
13
+ export interface WattsLocation {
14
+ locationId: string;
15
+ ownerId: string;
16
+ name: string;
17
+ address: {
18
+ address: string;
19
+ address2: string;
20
+ city: string;
21
+ state_province: string;
22
+ zipcode: string;
23
+ country: string;
24
+ };
25
+ awayState: number;
26
+ isDefault: boolean;
27
+ isShared: boolean;
28
+ userType: number;
29
+ supportsAway: boolean;
30
+ usersCount: number;
31
+ devicesCount: number;
32
+ }
33
+ export interface WattsDeviceData {
34
+ Sensors: {
35
+ Room: {
36
+ Val: number;
37
+ Status: string;
38
+ };
39
+ Floor: {
40
+ Val: number;
41
+ Status: string;
42
+ };
43
+ Outdoor: {
44
+ Val: number;
45
+ Status: string;
46
+ };
47
+ };
48
+ State: {
49
+ Op: string;
50
+ Sub: string;
51
+ };
52
+ Mode: {
53
+ Active: number;
54
+ Val: string;
55
+ Enum: string[];
56
+ };
57
+ Target: {
58
+ Active: number;
59
+ Sensor: string;
60
+ Hold: number;
61
+ Heat: number;
62
+ Cool: number | null;
63
+ Min: number;
64
+ Max: number;
65
+ Steps: number;
66
+ };
67
+ TempUnits: {
68
+ Active: number;
69
+ Val: string;
70
+ Enum: string[];
71
+ };
72
+ Units: string;
73
+ SchedEnable: {
74
+ Active: number;
75
+ Val: string;
76
+ Enum: string[];
77
+ };
78
+ Schedule: Record<string, unknown>;
79
+ Energy: Record<string, unknown>;
80
+ DateTime: string;
81
+ TZOffset: number;
82
+ }
83
+ export interface WattsDevice {
84
+ data: WattsDeviceData;
85
+ isConnected: boolean;
86
+ requestingUser: string;
87
+ deviceId: string;
88
+ name: string;
89
+ modelId: number;
90
+ modelNumber: string;
91
+ deviceType: string;
92
+ deviceTypeId: number;
93
+ location: {
94
+ locationId: string;
95
+ name: string;
96
+ address: unknown;
97
+ awayState: number;
98
+ userType: number;
99
+ };
100
+ imageUrl: string | null;
101
+ isShared: boolean;
102
+ }
103
+ export interface ThermostatDevice {
104
+ id: string;
105
+ name: string;
106
+ /** Current room temperature in °F */
107
+ currentTemperature: number;
108
+ /** Current floor temperature in °F */
109
+ floorTemperature: number;
110
+ /** Current outdoor temperature in °F */
111
+ outdoorTemperature: number;
112
+ /** Target/setpoint temperature in °F */
113
+ targetTemperature: number;
114
+ /** Operating mode: "Heat" | "Off" */
115
+ mode: string;
116
+ /** Whether the thermostat is currently calling for heat */
117
+ isHeating: boolean;
118
+ /** Whether the location is in Away mode */
119
+ isAway: boolean;
120
+ /** Whether schedule is enabled */
121
+ scheduleEnabled: boolean;
122
+ /** Whether the device is connected */
123
+ isConnected: boolean;
124
+ /** Model number */
125
+ modelNumber: string;
126
+ /** Min/Max setpoint range in °F */
127
+ minSetpoint: number;
128
+ maxSetpoint: number;
129
+ /** Location ID (needed for away mode) */
130
+ locationId: string;
131
+ }
132
+ export interface B2CTokenResponse {
133
+ access_token: string;
134
+ id_token: string;
135
+ token_type: string;
136
+ not_before: number;
137
+ expires_in: number;
138
+ expires_on: number;
139
+ resource: string;
140
+ id_token_expires_in: number;
141
+ profile_info: string;
142
+ refresh_token: string;
143
+ refresh_token_expires_in: number;
144
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,83 @@
1
+ import type { Logger } from 'homebridge';
2
+ import type { TekmarPlatformConfig, ThermostatDevice, WattsDevice, WattsLocation } from './types.js';
3
+ /** Convert Fahrenheit to Celsius (HomeKit uses Celsius internally). */
4
+ export declare function fahrenheitToCelsius(f: number): number;
5
+ /** Convert Celsius (from HomeKit) to Fahrenheit for the API. */
6
+ export declare function celsiusToFahrenheit(c: number): number;
7
+ export declare class WattsApi {
8
+ private readonly config;
9
+ private readonly log;
10
+ private accessToken;
11
+ private refreshToken;
12
+ private tokenExpiresAt;
13
+ /** Cookie jar — accumulated across all auth requests. */
14
+ private cookieJar;
15
+ constructor(config: TekmarPlatformConfig, log: Logger);
16
+ /**
17
+ * Full Azure AD B2C authentication flow:
18
+ * 1. GET /authorize → follow redirects, load login page, extract CSRF & cookies
19
+ * 2. POST /SelfAsserted → submit credentials
20
+ * 3. GET /confirmed → get redirect with auth code
21
+ * 4. POST /token → exchange code for tokens
22
+ */
23
+ authenticate(): Promise<void>;
24
+ /**
25
+ * Refresh the access token using the refresh token.
26
+ */
27
+ refreshAccessToken(): Promise<void>;
28
+ /**
29
+ * Ensure we have a valid access token before making API calls.
30
+ */
31
+ ensureAuthenticated(): Promise<void>;
32
+ apiRequest<T>(url: string, options?: RequestInit): Promise<T>;
33
+ /**
34
+ * Get all locations for the user.
35
+ */
36
+ getLocations(): Promise<WattsLocation[]>;
37
+ /**
38
+ * Get all devices for a location.
39
+ */
40
+ getLocationDevices(locationId: string): Promise<WattsDevice[]>;
41
+ /**
42
+ * Get a single device with fresh data.
43
+ */
44
+ getDevice(deviceId: string): Promise<WattsDevice>;
45
+ /**
46
+ * Fetch all thermostat devices across all locations.
47
+ * Returns normalized ThermostatDevice[].
48
+ */
49
+ getDevices(): Promise<ThermostatDevice[]>;
50
+ /**
51
+ * Normalize a WattsDevice into our simplified ThermostatDevice.
52
+ */
53
+ normalizeDevice(device: WattsDevice): ThermostatDevice;
54
+ /**
55
+ * Set the target temperature for a thermostat.
56
+ * @param deviceId - The device ID
57
+ * @param celsius - Target temperature in Celsius (from HomeKit)
58
+ */
59
+ setTargetTemperature(deviceId: string, celsius: number): Promise<void>;
60
+ /**
61
+ * Set the operating mode for a thermostat.
62
+ * @param deviceId - The device ID
63
+ * @param mode - "Heat" or "Off"
64
+ */
65
+ setMode(deviceId: string, mode: string): Promise<void>;
66
+ /**
67
+ * Set the schedule enable/disable.
68
+ * @param deviceId - The device ID
69
+ * @param enabled - Whether to enable the schedule
70
+ */
71
+ setScheduleEnabled(deviceId: string, enabled: boolean): Promise<void>;
72
+ /**
73
+ * Set the away state for a location.
74
+ * @param locationId - The location ID
75
+ * @param away - true for Away, false for Home
76
+ */
77
+ setAwayState(locationId: string, away: boolean): Promise<void>;
78
+ private setTokens;
79
+ /** Collect Set-Cookie headers from a response into the cookie jar. */
80
+ private collectCookies;
81
+ /** Format the cookie jar as a Cookie header string. */
82
+ private getCookieHeader;
83
+ }