Repository navigation
The current iot class system in Home Assistant doesn't show the full picture of how Home Assistant interacts with a device/integration. #1426
Unanswered
vemboy200
asked this question in
Documentation
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Problem statement
The
iot_classis currently a single manifest string that's implicitly answering 2 different questions at once:This vagueness with the
iot_classsystem causes problems:local_pollingorlocal_pushreads as "works offline," but several widely-used integrations contradict that.iot_classis also a single value per integration, but some integrations genuinely mix communication modes in a way one string can't represent.Two smaller issues in this same area didn't need architectural input, so I've already fixed them directly rather than raise them here: the local/cloud icon arrows pointed inconsistent directions with no rule behind it (now consistent), and the
iot_classdocumentation pointed at a 2016 blog post instead of a maintained docs page (now/docs/iot_class/), which also picked up expanded explanations and real examples along the way. Both are in https://github.com/vemboy200/home-assistant.io/tree/iot-class-revamp-p1.I'd like to raise and show my fixes for both remaining problems together, since a fix for one interacts with the other, and get your input before opening PRs for
home-assistant/home-assistant.ioandhome-assistant/core.Both of these are an architectural change in both documentation and possibly the
manifest.jsonof integrations, not feature requests. They add fields every integration author would need to correctly fill in going forward (iot_class_secondarywhere it applies,requires_interneteverywhere, with sensible auto-application covering most cases, so it's not a burden for the common case). I intend to implement this if the idea holds up, and already have working demos of both to show that, not just the idea.Problem 1: the current
iot_classcan't represent mixed communication modesExamples:
iot_class: local_polling.Which modes count as a secondary class
Only if the integration's advertised functionality would be lost, not merely degraded, without it. That distinguishes load-bearing (Roborock: local polling is gated by cloud auth, and it's the only path to control the Dyad/Zeo devices. Both pieces structurally necessary) from fallback/resilience (Sonos: correctly single-class
local_pushdespite falling back to polling if its push port is blocked. The docs describe this explicitly as a fallback, not a second required mode). An automatic fallback that exists purely for resilience shouldn't be declared, or the field becomes noisy for basically every integration with any reconciliation-polling pattern.Connection-liveness mechanisms, like a keepalive ping, don't count either, even though they can be a genuine request/response exchange. ESPHome's native API is a good example: on top of its push-based state updates, Home Assistant's client sends a
PingRequestafter a period of inactivity and expects aPingResponseback. But a ping carries no entity state, only connection liveness, so it isn't a secondary IoT class any more than TCP keepalives or MQTT's ownPINGREQ/PINGRESPwould be. Almost every push-based protocol needs some form of keepalive to detect a silently-dropped connection, so counting it would make this apply to nearly every push-based integration and stop being a meaningful signal.Proposed shape: a plain secondary list, not a ranked hierarchy
Two shapes seemed possible:
iot_class(unchanged, single value = primary) +iot_class_secondary(a plain, unordered list of any other load-bearing modes).primary_iot_class/secondary_iot_class/tertiary_iot_class/quaternary_iot_class/ etc.I'd propose (1). The existing
iot_classfield already is "primary" today, so nothing about multi-class needs to change there. The only genuinely new thing is a place to declare additional load-bearing modes, and a plain list does that with the smallest possible schema change. A ranked structure forces a fixed maximum number of ranks, for no practical benefit: nothing would ever need to read "is this the 2nd or 3rd secondary mode," only "is this mode declared at all."Choosing which one is the primary IoT class
Since
iot_classusually doesn't change, this isn't a schema question, it's author guidance. A suggested precedence order for which mode to declare asiot_classwhen an integration has several and it's not obvious which is primary. I've decided on:local_push→local_polling→cloud_push→cloud_polling→assumed_state→calculated→configurableThis is guidance, not a hard rule. Ecovacs is a case where I'd override it. It is a primarily cloud-based integration (
iot_class: cloud_push), with a self-hosted mode givinglocal_push/local_pollingas secondary. Even though local ranks higher in the order above, real-world common usage is cloud-first, and self-hosting is a deliberate alternative most users don't choose, not the default.Checking both against the load-bearing test:
iot_class: cloud_push, secondary[local_push, local_polling]. For users who choose self-hosting, losing the local mode means losing the self-hosted functionality entirely, not degrading it. Load-bearing, not fallback.iot_class: assumed_state, secondary[local_polling]. Polling has to be set up explicitly by the user, so it isn't automatic, but for users who do set it up, losing it means losing real state feedback entirely. Load-bearing, not fallback.I have a working demo of this too:
iot_class_secondarydeclared on 6 integrations (Roborock, Reolink, Hue, KNX, Ecovacs, Motionblinds Bluetooth), each checked against the integration's own docs, plus sidebar rendering and a docs page section explaining the load-bearing test at https://github.com/vemboy200/home-assistant.io/tree/iot-class-revamp-p3.Open question: does this shape (single primary + unordered secondary list, gated by the load-bearing test) hold up, or is there a case it doesn't handle?
Problem 2: the
iot_classsystem doesn't tell you whether an integration needs the internetlocal_polling.local_pushintegration whose firmware-update entity checks GitHub's releases API, unrelated to core light control, which is fully local.iot_classislocal_pushand nothing else, but whether it actually needs the internet depends entirely on your broker and whatever publishes to it, which Home Assistant has no way to know. For example, with TeslaMate, the connection from TeslaMate to Home Assistant is local, but the connection TeslaMate itself depends on, to Tesla's own servers, isn't. MQTT's flexibility means the sameiot_classcan sit in front of a fully local setup or one that's quietly cloud-dependent upstream of the broker.Proposed field:
requires_internetA separate manifest field, decoupled from
iot_class, with granularitynone/setup/conditional/required(not a boolean, a boolean loses the "needed once, then never again" case thatnoneandrequireddon't cover).Auto-apply (every class gets a default; manual override is for exceptions, not mandatory for a whole class):
cloud_polling/cloud_push→requiredlocal_polling/local_push/calculated/assumed_state→noneconfigurable→conditional(its whole premise is "depends on your setup," soconditionalis the honest default rather than a guess, which the MQTT/TeslaMate example above is a concrete case of)Resolving
setupvs.conditional: these aren't two equally valid options for "needs internet at some point":setuppromises nothing is needed after the one-time step. If anything post-setup needs internet, however small or occasional (an update entity, an optional feature), that disqualifiessetupin favor ofconditional. Found this via Plex (plex.tv login at setup, but also a Sonos-cast feature that needs remote access enabled), then confirmed the same pattern in the sonos integration itself.setuprequires the internet step to be mandatory, not just the recommended path. If a manual/offline alternative exists (SwitchBot Bluetooth's manual encryption-key entry vs. importing it from a SwitchBot account), that'sconditional, notsetup, even though the cloud path is what most users take.I have a working demo of this (docs page, sidebar UI, auto-derivation logic, and 30 real integrations manually classified with individual justification, each checked against the integration's own docs or external sources) at https://github.com/vemboy200/home-assistant.io/tree/iot-class-revamp-p2. Four MDI icons already cover the visual language cleanly (
cloud-off-outline,cloud-key-outline,cloud-question-outline,cloud), which I mention because it means the eventual Core/frontend UI work isn't starting from a blank slate either.Problem found with update entities that needs resolving
A problem I found while researching how update entities should be classified is how the update is checked for. For example, in Plex, Home Assistant's own code never leaves the local network and it sends a local command or query (
checkForUpdate()against the Plex server's own base URL for Plex) and the plex server is the one that actually reaches its vendor's cloud, entirely on its own. Home Assistant only ever reads back a flag or version string that already arrived over the existing local connection. ESPHome and TP-Link'spython-kasalibrary (via its device-sideget_latest_fw/get_intl_fw_listcalls) follow the same shape. But in WLED, Home Assistant itself makes the outbound request: itsWLEDReleasesDataUpdateCoordinatorcalls GitHub's releases API directly to determinelatest_version, and only hands the install step back to the local device afterward. So the presence of an update entity doesn't tell you anything aboutrequires_interneton its own.Something like WLED's update entity may also require a secondary IoT class of cloud polling too because of it.
Virtual/brand integrations need to inherit, not duplicate
Something the demo surfaced that applies to both fields above: many integrations have "virtual" pages representing individual supported brands (
ha_integration_type: virtual, pointing at a real integration via a supporting-domain reference). For example, Motionblinds alone has 28 brand pages like Acomax, Brel Home, and Dooya, each nominally its own page but really just the same underlying integration under a different name.The first version of the demo declared
iot_class/requires_internetindependently on each page, which meant a manual override on the real integration (for example, Motionblinds ->requires_internet: setup) was silently ignored on all 28 of its brand pages, since each one's own copy ofiot_classfed back into auto-derivation and produced a different, wrong answer (none) than the real integration. Fixed by resolving classification through the supporting integration whenever a page declares it represents one (rather than whenever a page happens to be missing its own copy of the data, which doesn't work since virtual pages usually carry a stale duplicate anyway), so a virtual page always reflects its real integration's values, including future overrides, with nothing to duplicate or keep in sync by hand.That same pass surfaced a second, unrelated bug worth mentioning since it shows why this needs care: the lookup key isn't guaranteed unique. 32 pages share
domain: mqtt, 4 sharedomain: alexa(three of which are legacy sub-docs with no classification data at all), and coincidentally 2 sharedomain: switchbot(the Bluetooth integration and a completely separate, real Matter-based one). An unconditional "look up by domain" resolves to whichever page a collection happens to list first, which broke Alexa's sidebar entirely and had been silently misattributing the Bluetooth SwitchBot's cloud requirement onto the unrelated Matter one. The fix needed a real, unambiguous signal for "this page represents another integration" rather than any property of the domain itself.If Core's manifest schema has (or grows) an equivalent virtual/supported-brand concept, both of these are worth accounting for from the start rather than discovering them the same way: brand pages need to resolve back to their real integration's
iot_class/iot_class_secondary/requires_internet, not carry their own independent copies.Worth calling out too, this had to be fixed in two separate places, not one. The per-integration page and the site's browse/filter page build their data independently, so a brand page's own sidebar could show the right inherited value while the browse filter still silently excluded it (searching "Setup" only surfaced the 2 real integrations, not their 30+ combined brand pages) until both were fixed the same way. Any equivalent UI on the Core/frontend side (say, a device list and a separate search/filter view) should expect the same trap if they end up as separate implementations rather than one shared source of truth.
Problem 3: protocol-generic integrations are a harder, distinct case
Raised via HomeKit Controller/ecobee. Some HomeKit accessories work fully offline, but some (certain ecobee thermostats bridged via HomeKit) still depend on the vendor's own cloud despite speaking local HAP. Roborock, Sonos, TP-Link, and WLED above are all single-vendor integrations. One company controls the firmware, so the whole integration can be characterized. HomeKit Controller (and similarly Matter and possibly MQTT) is protocol-generic. One piece of code bridging to any compliant accessory from any manufacturer. Whether a given accessory needs the cloud depends entirely on that vendor's own implementation, which Home Assistant has no static way to know ahead of time.
While this increases the complexity of how these kinds of integrations are classified, the vagueness of the current system may lead to people buying a product thinking it works entirely locally and isn't dependent on the cloud, then realizing later that it is.
Open questions for maintainers
iot_classunchanged + unorderediot_class_secondarylist, gated by the load-bearing test) hold up, or is there a case it doesn't handle?requires_internetbe the granularity proposed here (none/setup/conditional/required), or something else?entity_category), or manifest-level only?iot_classwhere possible, as proposed, or always explicit?requires_internetmodel entirely, and instead be served by something like a crowd-sourced device compatibility database? Not proposing this is solved the same way as single-vendor integrations, flagging it as a distinct, unresolved category.Plans
manifest.jsonand shown on integrations in the Devices & services tab of Home AssistantAll reactions