Custom Home Assistant integration branded for BMS Smart Home, focused on stable local Tuya device control.
This project is a fork of LocalTuya
by xZetsubou, itself derived from
rospogrigio/localtuya and the
original pytuya work. The bundled
protocol layer under custom_components/bms_integration/core/pytuya/ also
carries work from TinyTuya, and the
device preset tables under core/ha_entities/ originate from the official
Home Assistant Tuya integration.
Like its upstream, this integration is distributed under the GPL-3.0
license (see LICENSE). Any redistribution must keep this attribution and
the license text.
- Local Tuya control for Wi-Fi devices and Tuya hub sub-devices.
- BMS Smart Home branding and Home Assistant integration metadata.
- Anti-flap availability handling for short Wi-Fi or hub reconnects.
- Availability diagnostics are written to
/config/bms_integration_availability.jsonl. - Local reconnect backoff to reduce noisy unavailable/available history entries.
- A connected Tuya gateway is actively checked every 30 seconds and reconnects automatically if it stops answering.
- Gate/garage cover support with a separate open/closed sensor DP.
- On-demand DP refresh service for supported Tuya devices.
- Cloud-assisted setup support.
- Integration domain is
bms_integration. - BMS Smart Home brand assets are included under
brand/. - Entity availability no longer drops immediately on a short socket/Wi-Fi disconnect.
- Devices enter a reconnecting grace window for 120 seconds before entities are marked unavailable.
- Successful status updates cancel pending shutdown/unavailable tasks.
- Reconnect attempts use a softer backoff: 1, 2, 5, 10, 20, 30, then 60 seconds and stay at 60 seconds for as long as the device remains unreachable.
- Lights and switches apply commands optimistically by default (the new state shows immediately and is corrected by the device's own status update); the state is rolled back if the command fails.
- Cover entities can use a dedicated gate sensor DP.
- The gateway watchdog and the startup recovery passes now actually run. Both were scheduled as plain sync listeners, which Home Assistant runs in an executor thread, so every firing failed with "loop is not the running loop" and a device left without a recovery task stayed unavailable until the integration was reloaded by hand.
- A repeated warning is logged at debug level instead of being dropped, and a sub-device that cannot reach its gateway now says why.
- Turning several lights of one device on or off together no longer bounces some of them back for a second: optimistic values are recorded in the device and protocol caches, so the first partial confirmation from the device cannot resurrect the stale cached state of its sibling channels.
- Adding and removing devices from the panel. A discovered device is added in place: name, key, address - and optionally a template, an already configured device of the same model whose entity set is copied over with the names rewritten for the newcomer. The panel verifies it can actually talk to the device before writing anything, refuses an address another standalone device already occupies (the integration would silently never start the second one), and removal also sweeps the device's entities and registry row instead of leaving them permanently unavailable.
- Изолированный режим (lockdown). The cloud is only a helper here - it
fetches keys and the device list during setup, while control is always local
- but on a site where nothing may leave the network, "only a helper" is not good enough. The switch forbids the exchange outright, in the single place the integration reaches the internet from, ahead of even the token refresh. It is a process-wide latch, so the client the config flow builds for itself is stopped too, and blocked attempts are counted and shown in the panel. A one-click button in the panel header - visible on every screen - toggles the mode: the latch engages immediately, ahead of the entry reload the settings write triggers, so not a single request slips through the window, and the flag persists across Home Assistant restarts. Local control is untouched. With a cloud account attached, keys stop being refreshed - replace such a device from the panel instead.
- Replacing a device keeps its identity in Home Assistant. Hardware breaks;
the wiring of an installation does not. Behind a device sits a long tail:
entity ids referenced by dozens of automations and cards, its room, history
and long-term statistics, learned IR codes. Two cases are handled from the
panel. Same device id but a new key - after a hub is swapped or a device is
re-paired - only updates the credentials and touches no registry at all. A
physical replacement gives the new hardware the old device's identity: the
entity configuration is carried over,
unique_ids and the device-registry identifier are rewritten, learned IR codes move with it, and a replaced gateway relinks its children. Home Assistant keeps treating it as the same device. - New Комнаты screen: every area with the integration's devices in it, assignment from the panel, and creating an area without leaving it.
- Настройки дома: the availability window, the startup window and the gateway watchdog interval are tunable per installation, and integration-wide debug logging is a switch rather than an edit to configuration.yaml. Unset values keep the previous behaviour exactly.
- New sidebar panel BMS Control Center (admin only): overview with health tiles, the full connection map from config entry down to each datapoint, a device card with status/entities/DPS/diagnostics/configuration, discovered devices, an event timeline over the availability report, and integration settings. Backed by admin-only WebSocket commands that never serialize local keys or cloud credentials.
- A gateway that reports its sub-devices in several reply frames no longer gets a fixed subset of them flap-disconnected: absence is now judged over the union of all frames of a poll cycle, across two consecutive cycles, and "nearby" devices count as present.
- A reachable sub-device that the gateway drops from its LAN status table is no longer failed on its initial handshake and left permanently disconnected (previously it only revived for a few seconds after a press in the cloud app). A sub-device shares the gateway's validated key, so an empty initial status is treated as a cold gateway table, not a bad key: the shared session is kept and commands go through, while a genuinely departed child is still caught by the offline/absent path.
- Adding a Zigbee gateway itself as a device no longer takes every sub-device behind it offline. A hub's own datapoints are often cloud-pull only, so its LAN status query returns nothing; that is no longer treated as a failed handshake when the device has sub-devices.
- A corrupt or hostile frame, a payload the device reports as "data unvalid",
or a fault in a listener no longer closes the socket. Everything in that
path runs inside
data_received, and on a Zigbee hub that socket is shared by every sub-device behind it, so one bad frame took a whole floor offline. - A command that fails no longer looks like a command that succeeded: a lost
write raises instead of returning None,
set_statusre-raises instead of swallowing, and the entity's optimistic value is rolled back for real. - A reply timeout on one sub-device no longer reconnects its gateway. The shared session is reset immediately only on a connection-level error, and otherwise after three failures in a row. The pilot site logged 29 lost commands and 138 gateway handshakes in one day before this.
- Repeated INFO messages are demoted to debug for an hour, so a permanent fault cannot fill the log; session-key material and the discovery table are no longer written to logs or diagnostics.
- An address heard over UDP is confirmed over several broadcasts before it is applied, and a device whose address keeps flipping is left alone instead of reloading the whole config entry once a minute forever.
- Two devices configured on one address no longer collapse into one object - the integration says which device is parked and why.
- Motion sensors, base64 power sensors and the reconnect loop no longer grow a list, respawn entities or spin at 1 Hz for the life of the installation.
- Covers, lights, thermostats, selects, fans and the IR remote survive a datapoint that is missing, empty or of the wrong type instead of freezing the entity until Home Assistant restarts.
- The panel escapes every value it renders, ignores a late answer for a device the operator has navigated away from, keeps focus and scroll during its background refresh, and stops polling while the tab is hidden.
- A gateway dropping mid-command no longer cancels the caller. Pending waiters were released by cancelling their futures, so a CancelledError travelled out of the service call - which in Home Assistant is not an error message but task cancellation, stopping a script halfway through a batch. They now get a connection error instead. A stress run of 18 315 commands across 30 flapping hubs surfaced 53 of these.
- The
set_dpandreloadservices are admin-only, matching the panel, and answer with a readable error instead of a bareKeyErrorwhen the device or its entry is gone. - UDP discovery rebinds itself if its socket dies. It used to be opened once and never checked, so a listener that died left the installation unable to notice an address change for the rest of the run.
- Dispatcher signals and remote storage/service domain use the BMS Integration domain.
- Replacing a device now verifies the new hardware answers before any registry is touched, refuses an address another standalone device already occupies, and absorbs the duplicate row a crash inside the replace window leaves behind - re-running the replace converges instead of colliding.
- Add, replace and remove serialize on one lock: each of them awaits a network probe between reading and writing the config entry, so two concurrent calls used to overwrite each other's device silently.
- The simulator can now poison the wire (
--corrupt-every N) and the stress harness gained full-fleet blackout and dirty-wire phases. Measured on 40 hubs / 720 devices: 28 224 commands through 5 rounds of flapping hubs with zero unexpected exceptions, 3 back-to-back full blackouts with full self-recovery, 7 072/7 072 commands delivered while garbage preceded every third reply, and a soak with zero task/listener/memory growth.
Home Assistant assigns el.hass before connectedCallback runs, i.e. before
the panel has built its DOM. refresh() painted the header first, so
getElementById("hactions") returned null and the setter threw - out into
Home Assistant's own property-assignment loop. Worse, _inflight had already
been latched true and its finally sat inside a try the code never
reached, so the flag stayed set forever and every later tick returned
immediately. Nothing was ever fetched.
Three fixes: the busy flag is released in a finally that wraps the whole
call, the header and navigation refuse to paint before the DOM exists, and the
setter can no longer throw into Home Assistant. Registering the element name is
also guarded now - on an upgrade Home Assistant loads the new module into the
same document, and the duplicate customElements.define threw, leaving the
old class running until a full page reload.
Home Assistant assigns el.hass as soon as it creates a panel element. If the
panel module has not finished loading, the element is not upgraded yet and
that assignment lands as an own property, shadowing the prototype setter
for good: the setter never runs, refresh() returns on its first line, and
the panel sits on its loading placeholder forever. The race gets likelier as
the file grows, which is why it only started biting recently. The element now
reclaims such properties on connect.
Found alongside it: a throw while painting the header or the navigation bar
aborted paint() before the main area was written, which looks identical to
the same freeze. Those two are now painted inside their own guard, and the
navigation no longer assumes the overview reply carries a summary.
The one failure a Zigbee installation hits most - a local_key that does not
match, typically because the child carries its own key instead of the
gateway's - used to be invisible. The 3.4/3.5 session negotiation failed, the
reply was dropped in the parser as "bad CRC/HMAC", status() returned an
empty dict, and the device sat in an endless reconnect loop with an empty
error in the panel. Seen on a live site: 72 devices unavailable, not one word
about why.
Now the negotiation failure is named at every level - the dropped negotiation
reply, the HMAC mismatch and the failed handshake all say the key does not
fit - the failure is raised instead of swallowed, the reason is stored so the
panel's device card shows it, and because the text names local_key the
coordinator also asks the cloud for a fresh one.
tools/stress_test.py --wrong-child-keys reproduces exactly that fault, and
--fake-gateways reproduces the topology it happens on: gateways that are not
configured as devices, so a child is promoted to carry the socket.
Re-pairing devices rotates every local_key at once while the device ids stay
the same. Adding them again would mean losing entity ids, automations, areas
and history for the whole site. Обновить ключи из облака matches the
configured devices against the cloud account by device id and updates the keys
in place: the devices in Home Assistant remain the same devices.
It previews first - how many will change, how many already match, how many are
missing from the cloud - and only writes on confirmation. Keys are never sent
to the frontend, in either direction. If a device now sits behind a different
gateway (its node_id changed in the cloud) that is called out separately: the
key alone will not bring it back, it needs a replacement.
The button appears on the overview whenever a cloud account is configured and something is off, and always in home settings.
A battery sensor sends its temperature often and its charge once a day, and the gateway only caches what it was given. A report about ONE datapoint wakes every entity of that device - so the entity whose datapoint was not in the report used to overwrite its own value with "unknown". Silence is not a new reading: an absent datapoint now leaves the value alone.
The same sensors also came up "unknown" after a restart and stayed that way until the device felt like reporting, even though the value had been known a minute earlier and the Tuya app shows it always (the cloud keeps the last one). The last known measurement is now shown immediately and is replaced by the device's first real report.
The sensor fix above turned out to be one case of a general rule, so the rule now lives in the base entity: a report about ONE datapoint wakes every entity of that device, and an entity whose datapoint is absent from that report keeps what it had instead of writing None over it.
The worst instance was the binary sensor: an absent datapoint read as the string "none", matched no "on" value, and the entity reported off. A leak sensor that had actually tripped therefore cleared itself the moment the device sent its next battery or temperature report. Fan, light, cover, climate and water heater carried the same pattern and are guarded too.
Adding "по образцу" copied the template's gateway_id, so a standalone device
became a false child of a gateway it does not sit behind. Replacing that
gateway then silently rewrote the impostor's address to the gateway's, taking a
working device down. Parentage now comes only from the hardware being added,
and a record claiming a gateway without a node id is rejected as malformed.
Replace and add read the configured devices, then go to the network to verify the hardware, then write. The config entry is written by others too - the UDP address change and the cloud key refresh both write it directly - so a write landing inside that window used to be silently reverted by the stale snapshot. Both now re-read the entry after the network round trip and re-validate before writing.
The control datapoint holds the last command, not the state. The entity left the moving state only when the reported position hit exactly 0 or exactly 100, so one lost position report stranded it there - and Home Assistant hides the direction arrow while a cover is moving. A movement is now bounded by the motor's own travel time in every positioning mode; no stop command is sent, because it is our belief that is stale, not the hardware.
A child that answers the initial status query with nothing is deliberately let through the handshake, or one sleeping sensor would tear down the shared gateway session for every sibling. But the real status query ran exactly once, at connect, so whoever came up empty stayed empty for good - the periodic refresh cannot help, since its interval is off by default and update_dps requires no answer. Such devices are now re-queried on a backoff, from one minute up to ten, and only while they hold no datapoints at all.
An air conditioner reported a target of 0 against its own range of 16-30. Zero there is a placeholder, not a reading, but it was accepted as real - and Home Assistant hides the setpoint control when the target falls outside min/max. The last good setpoint is now kept instead. The unit conversion also ran on every status update, including ones carrying no temperature, so an already-converted value drifted toward -40; it now applies only to a fresh reading.
Some air conditioners will not start until the target temperature changes: you turn them on and they sit there until the setpoint is moved by hand. A per entity checkbox makes the integration do it - the setpoint goes one device step up and immediately back to what the user set, after any command. The push lives at the command level and never reaches the thermostat card, and it is sized by the device's own precision, not by the display step, so the hardware actually notices it.
Turning the unit off stops the push: the return to the user's setpoint travels in the same packet as the off command, never after it. A setpoint write that lands after "off" is exactly what starts these air conditioners again, which looked to the owner like a unit that refuses to switch off.
A gateway changed address and started answering "connection refused". The whole house went down, and the log carried not one line about the cause - only "Trying to connect" forever. Exactly one error was logged, EHOSTUNREACH; everything else fell into silence, including a refused connection, a reset, and a timeout (TimeoutError is an OSError too). The reason now reaches both the log and the availability journal, once per connect attempt rather than once per internal retry.
The site outage had a second half. The gateway was only accepting connections in short windows; a probe every 5 seconds got in almost at once, while the integration - 60 seconds of backoff, doubled after a long outage - sampled once every two minutes and missed those windows for hours. The house came back because a connect attempt was made by hand at the right second, not because anything was repaired.
A device other devices depend on now keeps a short retry interval instead of backing off to minutes: while it is down, so is everything behind it. A refused or unreachable connection also no longer costs three back-to-back attempts - the answer is instant and authoritative, and the burst was pure load on a gateway that was already choking. And when a gateway stays down for fifteen minutes, the integration says so, with the reason and with the one thing it cannot do itself: power-cycle the hardware.
A site has 70 sub-devices behind one gateway. A sharp mass toggle disturbed the shared session for a moment, and every child then asked the Tuya cloud for a fresh local key: 82 failed handshakes produced 69 cloud round trips, each awaited inside a connect attempt, so recovery dragged on for tens of seconds.
The key was never the problem. A sub-device runs on the gateway's already-validated session key and never opens a session with its own, so a failure caused by a gateway that was not up yet says nothing about the key. The cloud is now asked only when the error actually implicates it; a genuinely rotated key is still caught where it always was, by comparing against the gateway's key. The real error text is also kept instead of being overwritten with "Sub device is not connected", which had left every child of a gateway showing the same line instead of what happened.
Each device counts its own failed commands, but the socket belongs to all of them: three unanswered replies from one lamp reset the shared session for every device behind the hub. During a sharp pass over a room the timeouts pile up at once, and the reset kills every command still in flight, which produces more failures. A reset causes a reset.
Measured on site: one sharp pass over 36 lights produced 297 reply timeouts, 88 closed sessions and 69 reconnects, and the lights stayed on. A live gateway is told from a dead one by fresh traffic - while it works through a queue it still answers its other children, and a dead one answers nobody. The shared socket is now left alone while anything has recently come through it; a genuinely broken transport is still reset at once, as before.
Reported from the site, and it turned out to be the real complaint: "the light does switch off, the status in Home Assistant is wrong, the hub itself has no trouble". A busy Zigbee hub carries the command out and is late with the acknowledgement, or never sends one. The optimistic value was rolled back at that moment, which turned a successful action into a false display - and nothing could correct it afterwards, because the device had in fact changed and therefore had no update to send.
A reply timeout now keeps the expected value and schedules one delayed verification per device, which asks the device for its real state once the queue has drained; asking immediately, per command, would only join the very jam that caused this. A genuine refusal - a reset, a closed session, a transport that is gone - still rolls back at once, because there the device really did not change.
An audit of a site with 238 devices, 844 entities and ten gateways produced five fixes, two of them to work shipped days earlier.
An address change no longer reloads the whole entry. Any device's new DHCP address used to rewrite the config entry, and a write to it reloads the integration: one lease renewal took all 238 devices down for a minute. The address is now applied to the live objects, only the affected gateway and its children reconnect, and the reload is skipped by a token.
A gateway that has been down since startup is now counted as down. The outage timer only ever started for a device that had previously been connected, so the "gateway down for fifteen minutes" notice never fired for a hub that never came up at all - the very case it exists for. On the site two hubs were down for over half an hour, 54 devices were unavailable, and nothing was reported.
A long-dead gateway is asked less often, and the availability journal no longer repeats itself. Two dead hubs were writing 156 identical entries every 23 minutes - about 5000 a day against a journal that rotates at 2 MiB, which crowded out everything worth reading.
A hub with no datapoints of its own can now be added as a device. Refusing it left nine of the site's ten gateways unconfigured, their role taken by arbitrary sub-devices - and the connection watchdog deliberately skips such stand-ins, so nine of ten sockets had nobody watching them.
And a slow background sweep verifies devices in turn, one every few seconds. The integration is push-only by design, but a lost push means a wrong state forever, because the device has already changed and has nothing more to send.
From a site: remote.ir_rf_remote_control showed "on" while no command could
ever leave, because the device never connected at all. The log named it - the
connection was made, the device answered the status query with nothing, and the
handshake was then declared failed:
Connected attempt to detect the device DPS
Total DPS: {}
Handshake with 192.168.1.15 failed due to: Failed to retrieve status
That is how an IR/RF blaster is built: its datapoints are write-only and it has no state of its own, so an empty answer is the correct one. The device's key was checked against the cloud and matched, which rules out the decryption failure the refusal exists to catch.
A device whose entities are all command-only now connects on an empty status. A switch or a light still does not: there an empty answer means a rotated key or an error frame, and refusing is right.
Home Assistant has an infrared entity domain: adapters publish an emitter, and
brand integrations - Samsung Infrared, LG Infrared and the rest - put their own
code databases on top of whichever emitter is available. Our blaster was
invisible to them, because it was published as a remote entity, a different
domain entirely. Asked to set up, Samsung Infrared aborted with no_emitters
before its first question.
A device configured with an IR remote now also gets an emitter entity. The core
hands over a command as raw microsecond timings plus a carrier; the blaster
speaks its own code instead - 16-bit durations, FastLZ-packed, base64 - so the
translation between the two lives in core/ir_codec.py, tested both ways.
The platform is added only where the core knows the domain, which arrived in 2026.4; older installations, down to the supported 2025.1, are untouched.
RF is covered too, by the second entity. Home Assistant keeps infrared and radio
on two separate domains - infrared and radio_frequency - built the same way,
and the blaster does both, so it publishes both. The difference that matters is
the frequency: a learned RF button carries its own, but the core passes one as a
separate field, and sending it the learned-button way would have put a 315 MHz
request out on 433.92.
From a site: a GLEDOPTO SPI controller would not turn blue. Home Assistant showed
blue while the strip stayed as it was, because the colour went to colour_data
(DP 5), the format of ordinary RGB bulbs. Addressable-strip controllers carry
that datapoint and ignore it; their colour lives in paint_colour_data (DP 61),
a binary HSV frame in base64. The layout was confirmed on the hardware byte for
byte - a hand-sent frame turned the strip blue - and is now encoded and decoded
by the light platform whenever the colour datapoint is 61. Auto-configure puts
paint_colour_data first for strip and string lights, since the first matching
code wins and these controllers also expose DP 5.
A Tuya Zigbee group switches every lamp in it with one radio transmission: the
app sends the gateway a single command carrying ctype: 2 and mbid, the
group's address inside the gateway. That would replace a burst of per-lamp
commands queuing behind one radio. Two admin-only panel commands exist to verify
it on real hardware before anything is built on top: cloud_groups asks the
cloud which groups a device belongs to and returns the raw answer, to see
whether it carries the group's local id; group_send sends one group command
through a member lamp's gateway socket, in either framing.
Copy this folder into your Home Assistant config:
custom_components/bms_integration
Then restart Home Assistant and add the integration named BMS Integration.
After publishing this repository, add its GitHub URL to HACS as a custom repository of type Integration, install it, then restart Home Assistant.
Add one Tuya hub and several sub-devices through BMS Integration. Then briefly interrupt Wi-Fi or hub connectivity for less than 120 seconds. Entities should keep their last known state instead of producing one-second unavailable entries in history.
If a device is truly offline for more than 120 seconds, it should still become unavailable.
For gate relays that have a separate open/closed sensor DP, configure the entity as a cover and set Gate open/closed sensor DP to the sensor DP. By default, true means open and false means closed, matching common Tuya gate sensors. If your device uses text values, set Gate sensor/action value for open and Gate sensor/action value for closed accordingly.
If the device also exposes a movement/action DP, set Gate movement/action DP. The default movement values are opening and closing.
If the reed switch is mounted the other way round - it reports closed while the gate is actually open - tick Invert gate sensor instead of swapping the open/closed values. The inversion applies to the sensor DP only: the reported state, the position (0/100) and is_closed all follow it. The movement/action DP is not affected.
This integration is a fork of LocalTuya and keeps the same config entry
layout, the same entity unique_id format and the same entry version, so an
existing LocalTuya installation can be carried over by renaming the domain in
Home Assistant's storage instead of re-adding every device. Entity ids are
preserved, which keeps automations, dashboards, history and long-term
statistics working.
-
Back up Home Assistant and stop it (the storage files must not be written while the script runs).
-
Put
custom_components/bms_integrationin place and removecustom_components/localtuya, so both integrations do not claim the same devices. -
Run a dry run, then apply:
python3 tools/migrate_from_localtuya.py /config python3 tools/migrate_from_localtuya.py /config --apply
The script backs up every file it touches as
<file>.bak, only rewrites entries whose domain islocaltuya, and leaves other integrations alone. Learned IR/RF codes and user device templates are carried over as well. -
Start Home Assistant and check the devices under Settings -> Devices & services -> BMS Integration.
The device configuration is carried over unchanged, but this fork does not behave exactly like upstream. The differences that show up on a migrated system:
- Optimistic commands. Upstream entries carry no
optimisticoption, so this fork's default (on) applies to lights and switches: a command shows in the interface immediately, and a command sent to an offline device raises an error instead of being dropped silently. Run the script with--optimistic offto write the option out explicitly and keep the previous behaviour; it can be changed per entity later. - Availability. Entities stay available through a 120 second reconnect window (300 seconds at startup) instead of going unavailable at once, and a connected gateway is probed every 30 seconds.
- Diagnostics. Availability events are appended to
/config/bms_integration_availability.jsonl(rotated at 2 MiB).
- Entries created by the pre-2022 version of LocalTuya (one config entry per device, entry version 1) cannot be converted automatically - the script reports them and those devices have to be re-added through the UI.
- If the installation runs a newer upstream whose entry version is above the one this fork understands, the script refuses to touch anything and says so: relabelling such an entry would leave Home Assistant unable to load it.
To roll back, stop Home Assistant and restore the .bak files.
Home Assistant and HACS do not take icons from this repository: they are
served from brands.home-assistant.io, which is built from the
home-assistant/brands repository.
While the domain is missing there, HACS shows "Icon not available" and the
integration page shows a generic icon - regardless of what is stored under
custom_components/bms_integration/brand/.
The ready-to-submit payload is in brands-submission/, already sized to the
specification (icon 256x256 and 512x512, logo 512x198 and 1024x396). See
brands-submission/README.md for the pull request steps. This is also why the
HACS validation job in CI runs with ignore: brands; that flag can be dropped
once the pull request is merged.
When a device disconnects, reconnects, or is finally marked unavailable after the grace period, BMS Integration appends a JSON line to:
/config/bms_integration_availability.jsonl
Each entry includes the UTC timestamp, device id/name, host, node id, gateway id, disconnect reason, disconnect duration, reconnect attempts, and whether the device was a Tuya hub sub-device. Use this file to compare Home Assistant availability events with router/Wi-Fi logs.
Use the bms_integration.update_dps service to ask a connected device to publish its current datapoint values. The optional dps field accepts a list such as [18, 19, 20]; omit it to use the device defaults. This is useful for a dashboard or targeted automation, but it does not replace the automatic gateway watchdog.
This repository is distributed under GPL-3.0.
Run the test suites (no Home Assistant installation required):
pip install cryptography
python3 tests/run_all.pySuites are discovered, not listed: a new tests/test_*.py runs automatically.
tools/stress_test.py runs the whole fleet without hardware: it starts N hub
simulators, builds real TuyaDevice objects on top of them and drives a cold
start, a command storm, hub outages, address changes (as entry reloads) and a
chaos phase where slow hubs flap under continuous load, checking that nothing
is left without a recovery task and that tasks, listeners and memory do not
grow. The default is 30 hubs and 720 devices.
python3 tools/stress_test.py --hubs 30 --per-hub 23 --chaos 5tools/tuya_device_sim.py simulates a Zigbee hub with sub-devices over
protocol 3.3, 3.4 or 3.5 - including a session-key handshake, a reply split
over several frames, children reported offline or only "nearby", and a device
that answers nothing at all - so the protocol can be exercised against a real
socket without hardware.
See tests/README.md for what each suite covers. CI runs these suites plus
hassfest and HACS validation on every push.