Skip to content

Refactor App - #24

Open
craigde wants to merge 30 commits into
tomwpublic:mainfrom
craigde:claude/fix-reported-error-0Q6e2
Open

Refactor App#24
craigde wants to merge 30 commits into
tomwpublic:mainfrom
craigde:claude/fix-reported-error-0Q6e2

Conversation

@craigde

@craigde craigde commented Apr 13, 2026

Copy link
Copy Markdown

No description provided.

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)
User tested install and confirmed the SmartHQ cloud returns
type="Dehumidifier" (exactly matching Alan_F's driver name), so no
alias case is needed in apps/smartHQ_app manageChildDevice(). Remove
the ROADMAP entry that flagged this as a known-unknown.
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