Shelly Devices¶
The built-in ShellyService controls Shelly Gen 2 devices (Plus Plug S, Plus 1PM Mini, Plus 2PM, etc.) over either their local HTTP RPC API or their native RPC-over-MQTT channel. No cloud account or internet connection required.
Breaking change:
ShellyService's constructor now takes(http, mqtt, logger)instead of(http, logger), and its engine service-factory signature changed from(http, logger) => ShellyServiceto a single context object(ctx: ShellyServiceContext) => ShellyServicewherectx = { http, mqtt, logger }— mirroring the existinghomekitfactory pattern. Update any code constructingShellyServicedirectly or registering it as a factory-form engine service.
Registering devices¶
HTTP transport¶
Register devices in a factory function passed to services.shelly in your entry point:
import { createEngine, ShellyService } from "ts-home-automation";
const engine = createEngine({
automationsDir: "./src/automations",
services: {
shelly: ({ http, mqtt, logger }) => {
const svc = new ShellyService(http, mqtt, logger);
svc.registerMany({
"living_room_plug": "192.168.1.50",
"tv_plug": "shelly-tv.local", // mDNS hostnames work
"desk_lamp": "http://192.168.1.52", // full URLs are normalised
"bedroom_shutter": "shelly-2pm.local:8080", // custom ports work
});
return svc;
},
},
});
await engine.start();
You can also register a single device:
import { createEngine, ShellyService } from "ts-home-automation";
const engine = createEngine({
automationsDir: "./src/automations",
services: {
shelly: ({ http, mqtt, logger }) => {
const svc = new ShellyService(http, mqtt, logger);
svc.register("kitchen_plug", "192.168.1.55");
return svc;
},
},
});
await engine.start();
MQTT transport¶
Shelly Gen2 devices also expose a native JSON-RPC channel over MQTT: commands are published to <topicPrefix>/rpc, responses come back on a single shared response topic, status changes push instantly via NotifyStatus (instead of being polled), and presence is tracked via an online LWT topic. This is useful when devices aren't reliably reachable by IP, or when you want instant (rather than polled) status updates in HomeKit.
Register an MQTT-transport device with the object-form overload, using the device's MQTT topic prefix (e.g. "shellyplus1-a8032abe54dc") instead of a host:
import { createEngine, ShellyService } from "ts-home-automation";
const engine = createEngine({
automationsDir: "./src/automations",
services: {
shelly: ({ http, mqtt, logger }) => {
const svc = new ShellyService(http, mqtt, logger);
// HTTP and MQTT devices can be mixed on the same ShellyService instance.
svc.register("living_room_plug", "192.168.1.50");
svc.register("garage_plug", {
transport: "mqtt",
topicPrefix: "shellyplus1-a8032abe54dc",
type: "switch",
});
return svc;
},
},
});
await engine.start();
registerMany() also accepts mixed HTTP/MQTT entries in array form:
svc.registerMany([
{ name: "living_room_plug", host: "192.168.1.50" },
{ name: "garage_plug", transport: "mqtt", topicPrefix: "shellyplus1-a8032abe54dc" },
]);
A device's transport is fixed at registration — there is no per-call override and no automatic fallback between transports. If an MQTT call fails or times out, it is not retried over HTTP, and vice versa.
Required on-device setup¶
Before registering a device with transport: "mqtt", enable MQTT + RPC-over-MQTT on the device itself (via its web UI or MQTT.SetConfig):
enable: true— enable the MQTT clientserver: "<broker-host>:1883"— your MQTT broker addressenable_rpc: true— enable the RPC-over-MQTT channelrpc_ntf: true— enableNotifyStatus/NotifyEventpush notificationsstatus_ntf: true— include full component status in notifications
The MQTT RPC src identifier¶
Every MQTT RPC request carries a src value that the device echoes back on the shared <src>/rpc response topic, so the application knows which response subscription is "ours". This value is configurable via the MQTT_SHELLY_RPC_SRC environment variable (default: "ts-home-automation"). If multiple application instances share one broker, give each a distinct src — otherwise, one instance may receive another's RPC responses.
import { createEngine, loadConfig, ShellyService } from "ts-home-automation";
const config = loadConfig();
const engine = createEngine({
automationsDir: "./src/automations",
services: {
shelly: ({ http, mqtt, logger }) =>
new ShellyService(http, mqtt, logger, config.mqtt.shellyRpcSrc),
},
});
MQTT RPC requests have a fixed 5-second timeout (not configurable) — if a device doesn't respond within that window, the call rejects with a descriptive error naming the device, topic prefix, method, and timeout duration.
Switch methods¶
| Method | Returns | Description |
|---|---|---|
turnOn(name, toggleAfter?) |
Promise<void> |
Turn on; optional auto-off after N seconds |
turnOff(name, toggleAfter?) |
Promise<void> |
Turn off; optional auto-on after N seconds |
toggle(name) |
Promise<void> |
Toggle the switch |
isOn(name) |
Promise<boolean> |
Check if currently on |
getPower(name) |
Promise<number> |
Current power draw in Watts |
getStatus(name) |
Promise<SwitchStatus> |
Full switch status |
getConfig(name) |
Promise<SwitchConfig> |
Switch configuration |
getDeviceInfo(name) |
Promise<DeviceInfo> |
Model, firmware, MAC address |
getSysStatus(name) |
Promise<SysStatus> |
Uptime, RAM, available updates |
reboot(name, delayMs?) |
Promise<void> |
Reboot the device |
Switch status fields¶
const shelly = this.services.get<ShellyService>("shelly");
if (!shelly) return;
const status = await shelly.getStatus("living_room_plug");
status.output // boolean — on or off
status.apower // number — active power in Watts
status.voltage // number — voltage in Volts
status.current // number — current in Amps
status.aenergy // { total: Wh, by_minute: mWh[], minute_ts: unix }
status.temperature // { tC: number, tF: number }
Example: auto-off after TV goes idle¶
import type { ShellyService } from "ts-home-automation";
export default class TvAutoOff extends Automation {
readonly name = "tv-auto-off";
readonly triggers: Trigger[] = [
{ type: "cron", expression: "0 23 * * *" },
];
async execute(): Promise<void> {
const shelly = this.services.get<ShellyService>("shelly");
if (!shelly) return;
const status = await shelly.getStatus("tv_plug");
if (status.output && status.apower < 5) {
this.logger.info("TV is idle, switching off");
await shelly.turnOff("tv_plug");
}
}
}
Cover / shutter methods¶
For Shelly Plus 2PM devices configured in roller mode:
| Method | Returns | Description |
|---|---|---|
coverOpen(name, duration?) |
Promise<void> |
Open; optional stop after N seconds |
coverClose(name, duration?) |
Promise<void> |
Close; optional stop after N seconds |
coverStop(name) |
Promise<void> |
Stop movement immediately |
coverGoToPosition(name, pos) |
Promise<void> |
Move to absolute position 0–100 |
coverMoveRelative(name, offset) |
Promise<void> |
Move by relative offset -100 to 100 |
getCoverStatus(name) |
Promise<CoverStatus> |
Full cover status |
getCoverConfig(name) |
Promise<CoverConfig> |
Cover configuration |
getCoverPosition(name) |
Promise<number \| null> |
Current position 0–100, null if uncalibrated |
getCoverState(name) |
Promise<CoverState> |
Current state string |
coverCalibrate(name) |
Promise<void> |
Start calibration (full open → close cycle) |
Cover states¶
"open" | "closed" | "opening" | "closing" | "stopped"
Cover status fields¶
const shelly = this.services.get<ShellyService>("shelly");
if (!shelly) return;
const status = await shelly.getCoverStatus("bedroom_shutter");
status.state // CoverState string
status.current_pos // number 0–100, or null if uncalibrated
status.apower // active power in Watts
status.pos_control // true if calibrated for position control
Example: close shutters at sunset via cron¶
import type { ShellyService } from "ts-home-automation";
export default class SunsetShutters extends Automation {
readonly name = "sunset-shutters";
readonly triggers: Trigger[] = [
{ type: "cron", expression: "0 21 * * *" },
];
async execute(): Promise<void> {
const shelly = this.services.get<ShellyService>("shelly");
if (!shelly) return;
await shelly.coverClose("bedroom_shutter");
await shelly.coverClose("living_room_shutter");
}
}