switchbot-control
First request
hermes plugins install asks for SWITCHBOT_TOKEN and SWITCHBOT_SECRET. The next command lists the account. It does not ask for approval and does not send a command. It writes the daily counter to usage.json before the call.
hermes plugins install file:///path/to/hermes-plugin-switchbot-control --enable
hermes switchbot-control devices
A command such as turnOn asks once. The approval names the device and the command. on and つけて are turnOn. off and 消して are turnOff. 開けて is refused. The reply names turnOn for a curtain.
Hermes tools for the SwitchBot cloud OpenAPI (api.switch-bot.com, v1.1). Built on that API. This is not a SwitchBot product, and it does not use Home Assistant. Hermes has a separate Home Assistant plugin. This one does not speak BLE. The catalog card's local BLE path is a different route. Teknium's card says the cloud OpenAPI "requires a SwitchBot Hub plus Cloud Service enabled for BLE devices." The SwitchBot API README says the same for BLE devices, and Beyond V9.0 it names that menu Third-party Services. This plugin does not enable that setting. The live check's empty list was an account with zero devices. This plugin cannot tell that apart from the setting being off. SwitchBot is a trademark of its owner.
Authentication and error shapes were checked on the live API, and hardware was not used. 認証とエラーの形は実際の API で確かめた。機器の状態と操作は仕様の実例によるもので、実機では未確認。
What it does
switchbot_deviceslists physical devices and infrared remotes. An optionaldevice_idalso reads that device's status. It reports the list from that call and does not compare it with an earlier list, so a drop to zero is a successful empty list. Onlyswitchbot_watchsays when a previous watch file still has rows.switchbot_commandsends one command from a fixed list, after Hermes approval.switchbot_watchreads meters, contact sensors, and plugs. It reports the plain Plugpowerstring, not Plug Mini on or off. A command can still turn a Plug Mini on or off. The scheduled watch does not call a model. Each watch run costs 0 tokens, and the unmanned run holds no tools.switchbot_commandis its own toolset. A cron turn hides that tool and refuses it, even after always.
A skill (switchbot-devices) is registered while the plugin is enabled. It adds one line to the skills index of every session and tells the agent to look these tools up with tool_search before web search. It stays listed even on a surface where the tools were turned off with hermes tools. There the calls are simply unavailable.
Schedule delivery: the plugin discovers Hermes platform plugins before it checks a delivery name, so a schedule can deliver to a platform such as telegram on Hermes main (v0.22.2 was not affected). An unknown name is refused.
Signing
The sign that the live API accepted is the Python 3 example in the SwitchBotAPI README: base64 of HMAC-SHA256 over token + timestamp + nonce, keyed with the secret. Headers are Authorization, sign, t, and nonce. The short uppercase token + timestamp example in that README was not sent.
On 2026-10-06, with an account that had zero devices, these responses came back. The token and secret are not in this repository.
| Call | Result |
|---|---|
| Signed GET /v1.1/devices | HTTP 200, statusCode 100, message success, deviceList 0, infraredRemoteList 0 |
| Wrong token, wrong secret, wrong sign, or a timestamp 10 minutes old | HTTP 401, JSON {"message": "Unauthorized"}, no statusCode |
| GET /v1.1/devices/000000000000/status | HTTP 200, statusCode 100, message success, body {} |
| POST turnOn to that same id | HTTP 200, statusCode 190, message Wrong deviceId, No this device, body {} |
An empty status body is not reported as a device state. statusCode other than 100 is not success, including 190 on HTTP 200. A redirect is not followed, and the key is not sent to another host. A proxy set in the environment is not used, so the token is not sent to it. Each call times out at 20 seconds. There is no retry. A command POST that times out, or that gets back a 5xx, a 408, a redirect, a body that is not JSON, or HTTP 200 with a statusCode other than 100 (such as 190), may already have reached the device and moved it. The result says so and asks you to check the device state before you send it again. A connection that drops after the POST still says the command may have reached the device. A 401 or another 4xx, other than 408, does not get that note. A read that times out or returns a 5xx does not get it either. statusCode 161 on a command POST says the device is offline, and 171 says the hub is offline. Neither says the command moved the device. A body over 1000000 bytes is discarded and is not parsed or saved. If the command POST succeeded and the usage.json update after it fails, the result does not say that no API call was made. It says the command may have reached the device. The counter is replaced from a temporary file. A watch tick makes at most 21 calls. If every call uses the full 20 second timeout, that is about 420 seconds. That figure is an estimate, not a cap. A command that is sent waits 20 seconds for the device list, then Hermes's approval wait, then 20 seconds for the command. That wait is approvals.timeout, and the default is 300 seconds, which is about 340 seconds. That figure is an estimate, not a cap. This plugin does not cap that setting. Raising it makes the command wait longer.
Commands
Allowed, with parameter default: turnOn, turnOff, press, pause, volumeAdd, volumeSub, channelAdd, channelSub, setMute, FastForward, Rewind, Next, Previous, Pause, Play, Stop.
Also allowed when the parameter matches a published v1.1 table: setPosition, setAll, and SetChannel (digits). setPosition is the Curtain and Curtain 3 form, 0,ff,0 through 0,ff,100, with mode 0, 1, or ff. It is not on the infrared table. Blind Tilt's up;60 and Roller Shade's 0 through 100 do not match that form, so they are refused. setPosition is sent only when the device type is Curtain or Curtain3. 0 is open and 100 is closed. setAll and SetChannel are on the infrared table. For setAll, that table says modes 0 and 1 are auto, then 2 cool, 3 dry, 4 fan, and 5 heat. Fan speed 1 is auto, 2 low, 3 medium, and 4 high. The example is 26,1,3,on. Temperature is Celsius. The table does not publish a minimum or maximum, so this plugin accepts 0 through 40.
lock, unlock, and deadbolt are refused unless config safety_devices is true. A device whose type token is lock, keypad, garage, or door is refused the same way, including turnOn on that device. User-defined infrared buttons (commandType customize) are refused. Anything else is refused. The check happens before a command POST.
Every allowed command returns one approve line. The rule_key is a digest of the device id, the command, and the parameter, except for a lock or door command, which gets a new key on every call. The device name and the device type are not in that digest. In an interactive chat, session and always apply only to that same plan. A different device id, command, or parameter asks again. A lock, unlock, deadbolt, or a command to a lock, keypad, garage, or door asks again even when the plan is the same, so always does not cover the next one. Choose once when a later call of an ordinary command should ask again. Choosing always writes one plugin_rule:switchbot_command: line into Hermes config.yaml command_allowlist. Later interactive calls with that same ordinary plan skip the question. always does not send a command from cron, hermes -q, or a webhook. The pre_tool_call hook returns one approve line in an interactive chat, and block on those unattended turns, before Hermes consults the stored always. After a yes, the handler sends only when the device, the command, and the parameter still match that line. If they differ, it posts nothing and returns isError. When the pre_tool_call dispatcher returns BLOCKED, Hermes does not call the handler. The approval text names the device, its type, and the parameter, and it sends that text unchanged. For Curtain and Curtain 3 it also says what the command does: turnOff closes the curtain (position 100), turnOn opens it (position 0), and setPosition names the position. If the question, or its HTML-escaped form, is longer than 300 characters, it is not sent. The command string is not shortened to make it fit. The failure is not retryable. It does not change or cap Hermes's approval wait. The default approvals.timeout is 300 seconds. A higher value makes a sent command wait longer. yolo (--yolo / /yolo) and approvals.mode: off skip the prompt only in a session where a person can answer. They do not send a command from cron, hermes -q, or a webhook. Hermes fails closed when its own approval helper is missing, renamed, or raising. This plugin does not read those helpers. Requires Hermes >=0.21.6, the first stable release with the public cron mark ctx.current_cron_execution. v0.21.4 already accepts an approve line. That mark is how a real cron turn is recognized when the process environment does not say cron.
An empty HERMES_HOME scan with Hermes v0.22.2 and with origin-main is Verdict: SAFE, 9 findings, Decision: ALLOWED — Allowed (community source, safe verdict). The findings are the token names in the changelog, the cron_cli.py subprocess that runs hermes cron, and test subprocesses. They stay, because the changelog names the token, scheduling starts that child process, and the tests need subprocess. This plugin has no allowlist of its own. Hermes still applies its gateway rule: an empty platform allowlist denies the sender, except a per-platform allow-all, an approved pairing, a platform's own access check, or GATEWAY_ALLOW_ALL_USERS. Webhook events skip that rule, and so do Home Assistant events on Hermes 0.21.4 and events from a platform plugin marked trusted_inbound on newer Hermes. Hermes treats a webhook session as unattended. This plugin refuses the command there, even after always. A webhook can still list, read, and watch. On Hermes 0.21.4 a Home Assistant session is not treated as unattended, so a command there reaches the approval step. The request is posted as a Home Assistant notification, nothing in that session can send /approve (it only receives [Home Assistant] … changed from … to … events), and Hermes refuses the command after approvals.timeout. A person who can already talk to the agent can request a command, and the approval happens in that chat.
The slash command is /switchbot-control. The handler is async. Network work runs through asyncio.to_thread, and only one slash runs at a time, so it does not fill the shared executor or stop the gateway loop. It can list devices, run one watch, or schedule and unschedule. It does not send a device command. It tells you to ask with the tool. A slash watch, a CLI watch, and a tool call from a normal chat do not write watch.json or watch_fail.json. The scheduled script does, so the next run can still report the same change or the same failure. Put the delivery target first, as in /switchbot-control schedule telegram every 10m. If the first word looks like a schedule (every, in, a number, *, @daily, or a weekday), no job is created. cli, cron, api_server, and an unknown name such as telegarm are refused. local, origin, all, a known platform, platform:chat_id, bot-chat, and a combination such as origin,all are accepted. bot-chat starts one model turn, and the agent can act on that text. The CLI is hermes switchbot-control with devices, watch, schedule, and unschedule. It does not take -p or --profile. --deliver and --schedule are arguments of schedule, so Hermes's global flags do not eat them. Hermes plugins.isolation: host does not register that CLI. The default is in_process, where the CLI is registered. The slash command remains either way. Printed hermes cron list, hermes cron status, and hermes cron remove lines include -p <name> when the active profile is not default. unschedule says that watch_fail.json stays. hermes cron list does not delete the job. While the plugin is installed, remove it with unschedule or hermes cron remove. After uninstall, unschedule is gone and hermes cron remove remains.
accepted: true means the API returned HTTP 200, statusCode 100, and message success. moved stays false. The plugin does not read the device after the command, so it does not claim the device moved.
Watch and the daily cap
The published personal cap is 10000 calls per day. The SwitchBot README says going over that limit returns Unauthorized. A 401 from this plugin therefore also tells you to check that account-wide cap. The over-cap body was not observed on the live checks. It counts its own calls on a UTC date, in usage.json, and writes the new count before the call. It cannot raise the cap. The first result that sees the remaining count at or below warn_remaining (default 1000) says so once that UTC day. SwitchBot's own reset timezone was not measured.
One watch tick is one device list plus up to max_status_reads status reads (default 8, hard max 20). A repeating cron schedule is refused unless this plugin can prove it is slow enough that those calls fit in the cap, and never faster than every 120 seconds. The default schedule is */5 * * * *, which is 288 schedule slots per day. Each watch run costs 0 tokens. The unmanned run holds no tools. hermes switchbot-control schedule writes switchbot-control-watch.py under HERMES_HOME/scripts and creates the job with hermes cron create --no-agent --script. This is the same when HERMES_HOME is HOME/.hermes and when HERMES_HOME points somewhere else. unschedule removes that job and that script in both places. Hermes runs that script with its own Python and delivers stdout as the message. Empty stdout sends nothing. A non-zero exit is a failed run. The job is created with no_agent=True, which requires a script. wakeAgent=false is not how this watch stays quiet. Running schedule again switches an existing job with hermes cron edit --script --no-agent. A job made by 0.1.x keeps its model turn until that next schedule. On that turn, pre_tool_call allows switchbot_devices, switchbot_watch, tool_search, tool_describe, and tool_call, and blocks every other tool. A saved job id is included. Another cron job and a chat stay unchanged. If hermes cron fails, unschedule still removes the watch script and says the job was not removed. The failure text keeps the cron command's stderr. If a run is still going, Hermes skips the next slot. hermes cron create has no per-job tool list, which does not matter here because this job holds no tools. switchbot_command is a separate toolset. A cron turn hides it and refuses it, even after always. The in-chat switchbot_watch tool still reads on demand and does not advance the cron files.
Meter types follow the OpenAPI list names, including Meter, MeterPlus, MeterPro, MeterPro(CO2), and WoIOSensor (Outdoor Meter). Hub 2 can report temperature, and this plugin does not treat that hub as a meter. Contact is Contact Sensor. Plugs are types whose name contains Plug.
Meter alerts use temperature_high_c, temperature_low_c, and humidity_high_pct only when those strings are numbers. A value that is not a number, in any of the four, stops the device list, commands, the watch, and schedule before any API call. unschedule still works. Contact openState and the plain Plug power string are reported only after a change from the previous sample. That power string is on the Plug status table. Plug Mini (US) and Plug Mini (JP) status tables do not publish power or powerState. powerState is on their webhook tables, and this plugin does not receive webhooks, so it does not report those plugs turning on or off. Plug Mini (EU) on/off is switchStatus, which this plugin does not read. That device's power field is watts, a number. This plugin compares power only when it is a string, so a change in that number is not reported. Plug weight is the OpenAPI field described as power consumed in a day, in watts, and alerts only when plug_weight_watts is set and the value crosses it. When only a numeric power value changes, the result says first that a numeric power field is not compared. It does not lead with a claim that the on/off string is unchanged. A plug with no power string and no weight threshold produces no power alert. powerState is not saved.
If watch.json cannot be read, the tick says so and does not claim that nothing changed. If the previous file had device rows and this list has no meter, contact, or plug, the tick says the account is not a normal empty one. It does not delete those rows.
watch.json stores the device id, device type, and the fields that were read (temperature, humidity, openState, power, weight, electricCurrent, voltage). It also stores faults (device id and statusCode) and list_gap when the list has none of those devices but older rows remain. It does not store powerState. It does not store the token, the secret, or the device name. The chat still contains the tool result. The device list in that result includes names the API returned. At most 200 rows are kept.
Disclosure —
One SwitchBot account, from SWITCHBOT_TOKEN and SWITCHBOT_SECRET. The watch script keeps those two, plus SWITCHBOT_API_BASE, and drops every other key from the profile .env out of the process environment and out of the secret scope before it reads the token. This plugin does not use BLE or Home Assistant, and it has no allowlist of its own. An empty Hermes gateway allowlist denies the sender, except a per-platform allow-all, an approved pairing, a platform's own access check, or GATEWAY_ALLOW_ALL_USERS. Webhook events skip that rule, and so do Home Assistant events on Hermes 0.21.4 and events from a platform plugin marked trusted_inbound on newer Hermes. A webhook session cannot send a command, even after always. Cron and hermes -q cannot either. always applies only while a person can answer. On Hermes 0.21.4 a Home Assistant session is not treated as unattended, so a command there reaches the approval step. The request is posted as a Home Assistant notification, nothing in that session can send /approve (it only receives [Home Assistant] … changed from … to … events), and Hermes refuses the command after approvals.timeout. Approval for a command is in that chat. While HERMES_PLUGIN_HOST_PROCESS is 1, commands are refused and no command API call is made. An in-process watch in that process returns an error and does not report the devices as unchanged, because the cron mark is not visible. Set plugins.isolation to in_process so a command can be approved in this process. The scheduled script runs outside that process and does not need the cron mark. Tools are on for every surface, including gateways. Turn the toolset off per surface with hermes tools. The slash command is outside that. It stays open until allow_admin_from is set for that scope (group_allow_admin_from in a room). user_allowed_commands only adds commands for people who are not admins. A room's group_user_allowed_commands also applies to a DM that set no command list of its own. Disable the plugin to remove the slash. The slash command is /switchbot-control. The CLI is hermes switchbot-control. Cron cannot send a command, even after always. Turn the command toolset off per surface with hermes tools. The SwitchBot API README limits this API to personal use and prohibits commercial use and large-scale applications. A cron watch failure (unauthorized, the daily cap, a broken counter, and the other watch errors) sets notify true on the first failure of a streak and false on the later ones. A manual check reports the failure and does not advance that streak. A later successful watch starts the streak over. While the streak lasts, notify turns true again once 24 hours have passed since the last notice. This plugin cannot tell whether Hermes delivered a notice. If one was not delivered, the streak stays quiet until the next 24-hour notice. hermes cron list shows a run whose result was not delivered. Wrong tool arguments are not counted in the streak. A command POST that times out, or gets a 5xx, a 408, a redirect, a body that is not JSON, or a statusCode other than 100, 161 or 171, may have moved the device. 161 and 171 (device or hub offline) are reported as offline instead. Check before sending it again. An empty watch list (no meter, contact, or plug) notifies once per empty stretch and says that Cloud Services being off looks the same. Later quiet ticks of that stretch are not a healthy watch. A watch failure clears that empty count, so the next empty list notifies again. A device statusCode such as 161, 171, 190, or 152 is a note for that device and does not stop the tick. The same status on the next tick does not notify again. A 401, the daily cap, or a network failure stops the tick. If the previous file still has rows and the list has no meter, contact, or plug, that notice is sent once, until the list has those devices again. The agent can call the tools itself. A device command does not start a child process. Scheduling starts hermes cron as a child process. That is not a sandbox for untrusted code. No price list is fetched. This plugin does not import Hermes approval or cron internals. A missing copy of them does not, by itself, stop a command. Creating, switching, and removing a schedule run hermes cron create, hermes cron edit, hermes cron list, and hermes cron remove. A command is sent only after the pre_tool_call hook approves in an interactive chat and the device, the command, and the parameter still match that line. The data directory is <HERMES_HOME>/plugin-data/switchbot-control for the active profile. If that directory cannot be opened, it is not available. The printed profile is the name Hermes reports at register. HERMES_PROFILE and HERMES_HOME are not turned into a profile name, and the word custom is not invented. tests/ is shipped and is not loaded by register(). Removing the plugin leaves the cron job, usage.json, watch.json, and watch_fail.json (the failure streak). While the plugin is installed, remove the job with hermes switchbot-control unschedule. After uninstall that command is gone, so remove the job with hermes cron remove. hermes cron list only shows the job. Delete the switchbot-control plugin-data directory to remove those files.
Checked, and not checked
Checked on the live API: a correct signature, a wrong token, a wrong secret, a wrong signature, a timestamp 10 minutes old, an unknown device status, an unknown device command, and an empty device list. Those shapes are kept separate from the offline fixtures.
Not checked on hardware: meter, contact, plug, curtain, blind, roller shade, and lock readings, and command success. The live account had zero devices. Those reads and commands are checked with fakes that follow the published examples.
Also not checked:
- The Unauthorized body after the account goes over 10000 calls in a day, and the clock time when SwitchBot resets that count. The over-cap body was not observed. A 401 says to check the token, the secret, the clock, and that daily limit.
- Clicking always on the approval screen. For an ordinary command, that click writes one
command_allowlistline for that device id, command, and parameter, and a later interactive call with the same plan skips the question. A lock or door command asks again. cron,hermes -q, and webhook do not send the command after that click. That click was not performed on hardware. - Whether a list
deviceTypeis the webhook exampleWoLockorWoCurtain. Safety uses the names in the OpenAPI tables (Smart Lock,Curtain,Curtain3). This plugin does not claim thatWoLockis refused as a lock. - The Video Doorbell specification page. An earlier fetch was 404.
- The SwitchBot trademark guideline text. The terms-of-use URL landed on another company's page. The API README's introduction limits the API to personal use and prohibits commercial use and large-scale applications. It did not forbid this plugin's name.
- Whether Hermes's delivery ledger sends a failed reply again. This plugin does not deliver on its own.
- Writing curtain
pauseas the specification's "PAUSE state", callingstatusCode190 an internal error, calling 152 "device not found", or keepingenableCloudServiceon a list result. - A future timestamp. The live failure on record is a timestamp 10 minutes old, which returned HTTP 401.
README from the reviewed commit f0ecbf2; it updates when the author re-pins.

TakeshiTGAL

