Skip to content

About

Fix branch for optomtr/bmsintegration (issue #2)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

BMS Integration

Custom Home Assistant integration branded for BMS Smart Home, focused on stable local Tuya device control.

Attribution and license

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.

Features

  • 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.

What changed

  • 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_status re-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_dp and reload services are admin-only, matching the panel, and answer with a readable error instead of a bare KeyError when 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.

Hardened after the brutal stress pass

  • 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.

The panel stuck on "Загрузка…" (the real cause)

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.

Also hardened along the way

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.

A wrong key now says so

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.

One button to re-key a site after re-pairing

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.

Sensors stop forgetting what they know

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.

Silence is never a reading, on any platform

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.

A device added from a template no longer inherits someone else's parent

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.

Panel writes no longer revert someone else's change

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.

A curtain can no longer be "closing" forever

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 silent sub-device is asked again

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.

A setpoint outside the device's own range is refused

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.

Problem AC: nudge the setpoint on every command

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 failed connection always says why

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.

A gateway everyone depends on retries often enough to get back in

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 gateway hiccup no longer sends every child to the cloud

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.

A busy gateway is not a broken one

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.

A missing acknowledgement is not a refusal

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.

Findings from a 238-device site

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.

A remote has nothing to report, and that is not a reason to refuse it

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.

The IR blaster is now an emitter Home Assistant can build on

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.

Addressable strips take their colour from paint_colour_data

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.

Probing Tuya Zigbee groups

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.

Install

Manual

Copy this folder into your Home Assistant config:

custom_components/bms_integration

Then restart Home Assistant and add the integration named BMS Integration.

HACS Custom Repository

After publishing this repository, add its GitHub URL to HACS as a custom repository of type Integration, install it, then restart Home Assistant.

First test

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.

Gate/Garage Covers

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.

Migrating from LocalTuya

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.

  1. Back up Home Assistant and stop it (the storage files must not be written while the script runs).

  2. Put custom_components/bms_integration in place and remove custom_components/localtuya, so both integrations do not claim the same devices.

  3. 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 is localtuya, and leaves other integrations alone. Learned IR/RF codes and user device templates are carried over as well.

  4. Start Home Assistant and check the devices under Settings -> Devices & services -> BMS Integration.

What changes in behaviour

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 optimistic option, 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 off to 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).

Limits

  • 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.

Icon and logo (HACS shows a placeholder)

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.

Availability Diagnostics

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.

On-Demand DPS Refresh

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.

License

This repository is distributed under GPL-3.0.

Development

Run the test suites (no Home Assistant installation required):

pip install cryptography
python3 tests/run_all.py

Suites 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 5

tools/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.

About

Fix branch for optomtr/bmsintegration (issue #2)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages