Skip to content

MFA auto-skip - #25

Open
craigde wants to merge 36 commits into
tomwpublic:mainfrom
craigde:claude/compassionate-noether-eh62pw
Open

MFA auto-skip #25
craigde wants to merge 36 commits into
tomwpublic:mainfrom
craigde:claude/compassionate-noether-eh62pw

Conversation

@craigde

@craigde craigde commented Jun 26, 2026

Copy link
Copy Markdown

MFA auto-skip

claude and others added 30 commits January 25, 2026 19:37
…iver

- Add TemperatureMeasurement capability so oven appears in Rule Machine
  temperature conditions, dashboards, and third-party integrations
- Add temperatureUnit attribute that tracks F/C setting from appliance
- Parse TEMPERATURE_UNIT ERD (0x0007) to determine unit preference
- Include unit in all temperature sendEvent calls
- Map upper oven raw temperature to standard "temperature" attribute
- Fix empty map pattern [[:]] to use cleaner [] syntax
Add TemperatureMeasurement capability and F/C unit support to oven dr…
- Create new SmartHQ app (apps/smartHQ_app) to replace parent driver
  - Handles OAuth2 authentication with GE SmartHQ
  - Manages WebSocket connection for real-time events
  - Creates and manages child appliance devices
  - Proper app preferences page for configuration

- Update smarthqHelpers library
  - Add support for app parent via getUserIdForChild() method
  - Maintain backward compatibility with legacy driver parent

- Update packageManifest.json
  - Add SmartHQ app as required component
  - Remove SmartHQ System driver from manifest
  - Bump version to 1.0.0

- Mark smartHQ_system driver as deprecated
  - Users should migrate to SmartHQ app
  - Driver kept for reference but not in manifest

This follows Hubitat best practices where cloud integrations
use apps for auth/connection management and drivers for devices.
- Change namespace from "tomw" to "craigde" across all files
- Add acknowledgment to tomw in all file headers and README
- Update packageManifest.json with:
  - New unique UUIDs for all components
  - GitHub URLs pointing to craigde/hubitat-SmartHQ
  - Updated author and namespace references
- Update README with:
  - Acknowledgments section crediting tomw
  - Updated installation instructions for app-based architecture
  - Complete version history preserving original contributions
- Update library includes to use craigde namespace
- Preserve complete change history from original author

Files updated:
- apps/smartHQ_app
- devices/* (all 9 device drivers)
- libraries/smarthqHelpers
- libraries/wsReinitialize
- smartHQ_system (deprecated)
- packageManifest.json
- README.md
Refactor architecture
The generic Exception catch block in initialize() was missing a
reinitialize() call, so when a NullPointerException occurred on
interfaces.webSocket, the connection died permanently with no retry.
Also wrap sendWssMap() in try/catch so child device commands don't
cause unhandled errors when the WebSocket is temporarily down.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
…webSocket' on null object

Fix WebSocket not reconnecting after NullPointerException
Removed outdated change history entries from smartHQ_app.
Architecture:
- Delete deprecated smartHQ_system driver (no users on this fork)
- Delete wsReinitialize library; inline its WebSocket reconnection code
  into smartHQ_app, where it was the only real consumer
- Move OAuth credentials and login flow out of smarthqHelpers into the
  app, so device drivers no longer carry secrets they never use
- Reduce smarthqHelpers to pure utilities (ERD codec, message builders,
  common parse/setter helpers)
- Extract duplicated setIntegerErd / parseIntegerErd into helpers
  (previously copy-pasted in hood, ice maker, portable AC)

App / WebSocket fixes:
- Generic exception in initialize() now triggers reinitialize() so a
  transient NPE no longer kills the connection permanently
- unschedule(initialize) at start of initialize() and in uninstalled()
  to prevent schedule pile-up
- Defer subscribe_all + get_appliance_list to webSocketStatus
  "status: open" so we don't send over an unopened socket
- Validate WSS endpoint before connect; reinitialize on missing endpoint
- Clamp token-refresh re-auth delay to a safe minimum
- Concatenate region + JSESSION cookies into a single Cookie header
  (Groovy map literal was silently dropping the region cookie)
- Add `def` to local vars in OAuth flow that were leaking to script scope
- Use `any` instead of `findAll` truthy-list trick in webSocketStatus

Helpers fixes:
- decodeErdSignedByte: 0x80 now decodes to -128, not +128 (off-by-one)
- parseDoorStatusByte / flushEvents: drop empty-map initializer that
  produced bogus sendEvent calls; flushEvents now skips entries with
  no name as defense in depth
- parseSabbathMode / parseControlLock: wrap in try/catch

Driver fixes:
- oven: try/catch on parseCookState and parseCooktopStatus, init
  numberOfButtons for PushableButton capability
- laundry: try/catch on parseMachineState, parseDoorLock,
  parseSmartDispenseTankStatus
- refrigerator: bounds-check adjustSetpoints, declare missing
  sabbathMode attribute, route SABBATH_MODE ERD to parseSabbathMode,
  fix parseFilterStatus null/array safety, drop bogus events
  initializer, add F unit to temperature events
- portable AC: setCoolingSetpoint no longer forces "cool" mode (was
  silently overriding energy_saver/dry), bounds-check adjustSetpoint,
  add fallback "unknown" on enum lookups, drop duplicated setIntegerErd,
  add F unit to temperature events
- microwave: drop blocking pauseExecution(500) pattern; warn and bail
  when required state is unknown instead of pretending to refresh-and-wait
- home water filter: wrap parse in try/catch, add `def` to local vars
  that were leaking to script scope
- hood: fix cycleSpeed null check (indexOf returns -1, not null), don't
  pre-emit speed event before appliance confirms, move
  supportedFanSpeeds out of setSpeed into refresh/installed/updated,
  drop duplicated setIntegerErd / parseIntegerErd
- ice maker: drop duplicated helpers, route through logDebug, wrap
  OIM_POWER parsing
- dishwasher: mark Operation parameter as required, wrap parse methods
  in try/catch, drop bogus events initializer, drop unused locals

Package manifest:
- Bump to 1.0.1, refresh dateReleased and release notes

Note: token / WSS credential debug logging is intentionally retained
for v1 troubleshooting; will be addressed before release.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
The packageManifest used to deliver the helpers library via a bundle
ZIP at hubitat_SmartHQ.zip. That ZIP is no longer maintained, so HPM
would have failed to install. Drop the bundles entry and add an
explicit libraries entry pointing at the helpers source on main.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
Refactor App
HPM does not support libraries directly in packageManifest.json (the
HPM source has zero references to "librar"). The library entry I added
in 99e3f1b was being silently ignored, leaving #include directives
unresolvable on install. Restore the bundles entry so the helpers
library ships via a Hubitat bundle ZIP, and document the bundle
regeneration steps in README so it's clear when/how to rebuild.

The bundle only contains smarthqHelpers; apps and drivers continue to
install as plain files, so day-to-day code changes don't require
re-bundling.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
Restore bundle ZIP for shared library; document regeneration
Hubitat rejects "Cannot parse library definition" when the library()
block contains empty-string values for `category` or `documentationLink`.
The previous block (carried over from the original fork) had both empty,
which only worked when delivered via a bundle ZIP - the bundle import
path is more lenient than direct paste into Libraries Code. Give both
fields real values so the library can be loaded either way.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
Fix library() block so Hubitat parser accepts it
Hubitat's library parser rejects the file with "Cannot parse library
definition" when there's a /* ... */ comment block before the library()
declaration. Move the attribution / explanation to // line comments
*after* the library() block, and add a warning so the next person who
edits this file doesn't reintroduce the issue.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
The exported bundle was committed to main as SmartHQHelpersLibrary.zip,
not the placeholder name hubitat_SmartHQ.zip the manifest was using.
Update both the manifest and README to match.

https://claude.ai/code/session_01TArAXAXbb51BEW6dryM3GU
Add bundle
Hubitat's docs are explicit: interfaces.webSocket is driver-only, not
available in apps. The previous architecture (refactored from tomw's
original driver to an app) was broken from the start - any attempt to
open the WebSocket threw "Cannot get property 'webSocket' on null
object" because interfaces is null in app context. The earlier fix
to call reinitialize() on generic exceptions just turned the fatal
error into an infinite retry loop with the same NPE on each cycle.

New architecture:
- The app keeps the credentials UI, OAuth flow, message dispatch to
  appliance child devices, and reconnect scheduling
- A new "SmartHQ Connector" driver (devices/smartHQ_connector) is
  the only thing that talks to interfaces.webSocket. The app spawns
  one connector child automatically (isComponent: true so the user
  can't accidentally delete it) and hands it the WSS endpoint after
  obtaining a valid OAuth token
- Connector forwards Hubitat's parse() and webSocketStatus()
  callbacks to the app via connectorParse() / connectorOpened() /
  connectorDisconnected()
- Appliance child drivers continue to call parent?.sendWssMap() on
  the app; the app just routes to connector.sendWssMessage()

Manifest:
- Added SmartHQ Connector driver entry, marked required: true
- Bumped to 1.0.2; release notes describe the architecture change

README:
- Documented the connector in manual install steps and added a brief
  Architecture section explaining the app + connector + appliance
  child layout
Move WebSocket out of app into SmartHQ Connector child driver
Audited each driver against typical Hubitat automation use cases and
added the missing standard capabilities so dashboard tiles render
correctly and "is X running?" / "filter needs replacing" automations
work natively without custom rules on string attributes.

Refrigerator: added TemperatureMeasurement (mirrors fridgeActual into
the standard `temperature` attribute) and FilterStatus (rolls air +
water filter state up into the standard `filterStatus` attribute as
normal/replace; detailed per-filter values still in
airFilterStatus / waterFilterStatus).

Oven: added Switch driven by upper/lower cookMode != "off". on/off
log a warning - ovens cannot be started remotely via SmartHQ.

Dishwasher: added Actuator and Switch. on/off map to the existing
start/stop commands. Switch attribute is on while the cycle is in an
active wash phase (excluding pause/delay/end so "running" is accurate).

Laundry: added Switch driven by machineState run/delay states.
on/off log a warning - laundry can't be started remotely.

Microwave: added Switch driven by cookStatus. on/off log a warning
(safety: microwaves shouldn't be remotely startable).

Portable AC: dropped redundant RelaySwitch (Switch capability already
provides on/off), added ThermostatMode and FilterStatus capabilities,
emit thermostatMode attribute alongside thermostatOperatingState,
normalized filter values from "clean"/"ok" to standard "replace"/
"normal".

Ice Maker: added Actuator and FilterStatus capabilities, normalized
filter values from "ok"/"expired" to standard "normal"/"replace".

Hood: added Actuator capability.

ROADMAP.md: tracks Phase 2 (component child devices for refrigerator
doors, hood/icemaker lights, oven cavities) and other deferred items
including the token-logging cleanup before wider release.
Phase 1 capability cleanup across all appliance drivers
Background: a user reported MissingMethodException calling
getDataValue("userId") on the app during first install. Root cause is
the pre-restructure version of getUserId() which had a fallback chain
parent?.getUserIdForChild() ?: parent?.getDataValue("userId") - and
parent (the app) doesn't have getDataValue. The current library has
the fallback removed, so the immediate trigger is a stale bundle ZIP
holding the old library code on the user's hub.

Add a defensive layer for the underlying race condition that will
exist with any version of the library: between WebSocket open and the
List-appliances response, state.userId is null. Previously commands
in that window built JSON messages with null userId and either
silently failed or got rejected by the API. Now buildDevDetails()
returns null when userId isn't ready, which buildErdSetter and
buildAppCtrlSetter already treat as "skip and return null" (their
existing [devDetails, erdDetails].contains(null) check).

Also add a release-checklist item to ROADMAP.md reminding maintainers
to regenerate SmartHQHelpersLibrary.zip whenever the library changes
- HPM installs bundles before plain files, so a stale bundle silently
ships old library code to every new install.
Alan_F from the Hubitat community wrote and tested a dehumidifier
driver and shared it for inclusion in the package. Integrated with
minor consistency edits:

- Declared capabilities Actuator, FanControl, and FilterStatus
  alongside the attributes that already backed them. The original
  declared only Refresh, RelativeHumidityMeasurement, Switch, and
  TemperatureMeasurement, so commands and filter status didn't
  surface as capability-typed features to dashboards and rules.
- Normalized filterStatus values from "ok"/"replace" to the standard
  Hubitat FilterStatus vocabulary "normal"/"replace" to match the
  rest of the project (refrigerator, ice maker, portable AC, etc).
- Wrapped the ERD switch dispatch in a try/catch so a single
  malformed message can't take out the whole parse, matching the
  defensive pattern used across the other drivers.
- Fixed a small logInfo() glitch where the else branch emitted a
  debug log every time info logging was disabled (accidentally
  noisy). It now simply no-ops when disabled.
- Replaced hardcoded hex strings in the parse switch with @field
  constants (DH_TARGET_HUMIDITY, DH_CONTINUOUS_MODE_A/B, etc) so the
  ERD vocabulary is visible at a glance alongside the existing
  DH_POWER_STATUS / DH_FAN_SETTING constants.
- Added appliance unit to temperature events (matches refrigerator
  and portable AC).
- Simplified installed()/updated()/refresh() - the original pre-
  populated a large set of default attribute values which risked
  asserting state we didn't actually know (e.g. filterStatus="ok"
  before the first ERD arrives). Now refresh() only emits
  supportedFanSpeeds and calls refreshAppliance(); parse events
  drive everything else.

New features beyond the other drivers:
- Info-level logging preference (Alan's design): when enabled,
  mirrors each attribute change to log.info so external log
  aggregators can capture detailed device state without running
  debug logging globally. See ROADMAP.md for whether to roll this
  pattern out to the other drivers.
- User-selectable F/C temperature unit via driver preferences.

Manifest:
- Registered SmartHQ Dehumidifier as an optional driver
- Release notes credit Alan_F

README / ROADMAP:
- Listed Dehumidifier in Supported Appliances
- ROADMAP notes that if the SmartHQ cloud returns a type string
  other than exactly "Dehumidifier" (e.g. "Home Dehumidifier"),
  apps/smartHQ_app manageChildDevice() will need an alias case
- ROADMAP tracks the decision on whether to adopt Alan's info-
  logging preference across the rest of the drivers
Add SmartHQ Dehumidifier driver (contributed by Alan_F)
The login redirect query-string parser in get_authorization_code()
indexed split("=")[1], which threw ArrayIndexOutOfBoundsException
("Index 1 out of bounds for length 1") when GE's redirect contained an
empty-valued param such as "scope=" (String.split drops trailing empty
fields). This only ran on the full re-auth path, so connections kept
working on a valid refresh token but could not recover once a refresh
failed and a full login was forced.

Parse on the first '=' only and tolerate valueless params. When login
returns no authorization code (MFA challenge, bad credentials, or a
changed flow), log a clear warning and show the reason on the config
page instead of silently entering the reconnect backoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQiHrYrM3WmH5F3TPhEohL
craigde and others added 6 commits June 25, 2026 15:44
Fix full re-auth crash on empty-valued OAuth redirect param
GE re-enabled the non-persistent "We are testing MFA" interstitial,
which lands the login on an MFA redirect instead of the authorization
code, breaking re-auth.

Integrate youzer-name's MFA-skip approach (PR #12): when the
g_authenticate redirect points at the MFA prompt, fetch the MFA page
for a fresh _csrf, merge the session cookies, and POST an empty mfaType
to the redirect endpoint to replicate the website's "Skip" button,
continuing to the real code redirect.

Kept the integration's existing fixes that PR #12 had dropped:
- single concatenated Cookie header on g_authenticate (a duplicate-key
  map literal silently drops the region cookie)
- defensive redirect parsing via a shared parseRedirectParams helper
  (split on first '=' only; tolerate valueless params) so neither the
  primary nor the MFA-skip redirect can hit the temp[1] crash

Bumped to 1.0.5 with release notes crediting youzer-name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQiHrYrM3WmH5F3TPhEohL
Auto-skip GE's MFA test prompt during login
1.0.5 was already recorded as the installed version on some hubs, so HPM
would not re-offer it. Bump to 1.0.6 (same changes: full re-auth crash
fix, MFA test-prompt auto-skip, clear login-failure reporting) to force
the update to surface. No functional code changes beyond the version
label and release date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQiHrYrM3WmH5F3TPhEohL
Bump version to 1.0.6 to trigger HPM update
The app-based architecture moved reconnect logic into the app, so the
per-device initialize() command users relied on to kick a stale
connection from a rule is gone (the connector is a hidden component
device and its initialize is a no-op).

Add a "reconnect" command to every appliance driver that calls
parent.initialize(), re-authenticating and rebuilding the shared cloud
connection. The integration uses a single WebSocket, so triggering it on
any one appliance reconnects them all. Restores the manual "kick the
connection" workflow (e.g. a rule that fires after N hours of no
activity) pending the planned automatic idle watchdog in the app.

Bumped to 1.0.7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQiHrYrM3WmH5F3TPhEohL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants