[RE-RELEASE] EcoWitt and Wittboy Weather Stations And Sensors (Local)

Hey folks, figured I would throw this here for awareness. The new Ecowitt WS90 powered by Shelly works with Hubitat. Direct zigbee connection, or bluetooth.

I have little HTML weather tile setup with forecaster, works very well for my needs.

image

2 Likes

Hi @sburke781,

I understand you haven't had the opportunity to review my previous proposals yet. In the meantime, I have implemented several additional changes focused specifically on performance improvements.

Currently, my four Ecowitt devices and gateway account for over 55% of the total busy time across all 225 of my devices. To try and change this scenario, I have incorporated various optimizations, including some suggested by AI, ensuring that functionality remains unaffected.


High-Level Summary

  1. Major performance optimization β€” Replaced repeated device.currentValue() database reads with an in-memory ConcurrentHashMap attribute cache. On systems with many sensors, this eliminates hundreds of redundant DB lookups per data cycle.

  2. Sensor key dispatch rewritten for O(1) lookup β€” The gateway's main attributeUpdate() method used a giant switch/case with ~50+ string cases. Known single-sensor keys are now resolved via a static Map (SENSOR_KEY_MAP) in constant time, with regex only used for multi-channel sensors.

  3. New "Enabled Sensors" preference β€” A gateway-level setting lets you list only the sensor types you actually own (and want to report). Data for unlisted sensors is skipped entirely, saving CPU cycles and preventing creation of child devices you don't need.

  • Avoids attribute/regex matching for sensors that don't exist or don't report.
  1. Logging system rewrite β€” The string-based logging level ("debug", "trace", etc.) has been replaced with integer-based levels (1–5). The level is cached to avoid string comparison on every log call. Log message arguments are now closures (lazy evaluation), so debug/trace string interpolation is never executed at lower levels.

  2. Sensor state tracking moved from state.* to in-memory maps β€” The sensor driver's orphan-detection flags (state.sensor, state.sensorTemp, state.sensorRain, state.sensorWind) are now kept in a @Field static ConcurrentHashMap, avoiding persistent state writes on every data cycle.

  3. New per-sensor toggle preferences for VPD and Soil AD reporting.

  4. VPD unit conversion fix β€” The original had metric/imperial labels swapped.

  5. Regex patterns are now pre-compiled

  6. Minor bug fixes and code cleanup across both drivers.

Detailed Changes

Detailed Changes

Gateway Driver (ecowitt_gateway.groovy)

Performance: In-Memory Attribute Cache

  • Added @Field static final ConcurrentHashMap attributeCache as a per-device write-through cache.

  • New helpers: getAttrCache(), invalidateCache(), cachedString(), cachedNumber().

  • attributeUpdateString() and attributeUpdateNumber() now check the cache before reading from the database, and update the cache on write.

  • device.currentValue() calls throughout the driver (versionUpdate(), dniUpdate(), devStatus(), etc.) replaced with cached equivalents.

  • Cache is fully invalidated on updated() to ensure preferences changes are picked up cleanly.

Performance: O(1) Sensor Key Dispatch

  • Added @Field static final Map SENSOR_KEY_MAP mapping ~50 known data keys (e.g., "wh25batt", "tempf", "rainratein", "winddir", etc.) directly to their sensor ID.

  • In attributeUpdate(), the first thing checked is this map β€” if found, the sensor closure is called immediately with zero regex matching.

  • Only multi-channel sensor keys (which require extracting a channel number from the key name) still use regex. Those regexes are now pre-compiled @Field static patterns instead of being compiled inline on every call.

  • A matchedChannel() helper extracts the channel number from the last regex match, replacing verbose java.util.regex.Matcher.lastMatcher.group(1).toInteger() repeated everywhere.

Performance: Enabled Sensors Gating

  • New preference: "Enabled Sensor Types" β€” a comma-separated list like WH25,WH26,WH40,WH80,WS90.

  • Data keys for disabled sensor types are skipped before any child device lookup or creation.

  • Multi-channel regex blocks are gated by enabledIds.contains(sensorId), so the regex patterns aren't even evaluated for sensor types you don't use.

  • The enabled set is cached (cachedEnabledSensorIds) and rebuilt only on updated().

  • Fully backward-compatible: if the preference is empty/unset, all sensor types are enabled by default.

Performance: Logging System

  • Logging level changed from string enum ("error", "warning", "info", "debug", "trace") to integer enum (1–5).

  • Cached in @Field static int cachedLoggingLevel, updated only on updated() or logDebugOff().

  • logger() method now uses integer comparison instead of string equality checks.

  • All logger() calls across the driver now pass closures { "message ${expensive}" } instead of pre-interpolated strings, so string building only happens if the message will actually be logged.

Performance: Metric Cache Invalidation on Children

  • When the gateway's updated() runs (e.g., user changes Imperial ↔ Metric), it iterates child devices and calls invalidateMetricCache() on each, ensuring stale cached values are cleared.

Feature / Fix: dateutc Storage

  • Changed from state.dateutc = it.value to device.updateDataValue("dateutc", it.value) with a change-detection guard to avoid unnecessary writes. Using updateDataValue instead of state keeps this out of the mutable state map that gets cleared.

Fix: sensorIsBundled() Signature

  • Removed the unused channel parameter from sensorIsBundled(Integer id, Integer channel) β†’ sensorIsBundled(Integer id).

Cleanup: Removed devStatusIsError()

  • This method was unused and has been removed.

Fix: switch β†’ return in attributeUpdate()

  • The data.each {} closure used break statements inside the switch, but in a Groovy closure break has subtly different semantics than in a regular loop. Changed all break to return (which exits the current closure iteration) for correctness and clarity.

Fix: Typo in Comment

  • "end-od-data" β†’ "end-of-data", "childs" β†’ "child".

Sensor Driver (ecowitt_sensor.groovy)

Performance: In-Memory Attribute Cache

  • Same pattern as the gateway: @Field static final ConcurrentHashMap attributeCache with per-device sub-maps.

  • attributeUpdateString() and attributeUpdateNumber() now use cachedString() / cachedNumber() instead of device.currentValue().

  • All internal reads (e.g., cachedNumber("temperature") in dew point calculation, cachedNumber("batteryTemp") in lowest-battery calculation, cachedString("batteryTempOrg"), etc.) go through the cache.

Performance: Sensor State Flags in Memory

  • state.sensor, state.sensorTemp, state.sensorRain, state.sensorWind replaced with @Field static ConcurrentHashMap sensorTracker.

  • New helpers: getSensorFlags(), initSensorFlags().

  • Eliminates dozens of persistent-state writes per data cycle (Hubitat's state map writes to the database).

Performance: Metric System Cache

  • unitSystemIsMetric() now caches the result from parent.unitSystemIsMetric() in @Field static ConcurrentHashMap metricCache.

  • The parent gateway calls invalidateMetricCache() on all children when settings change.

  • Reduces ~15–20 cross-device parent calls per data cycle to 1 (on cache miss).

Performance: Logging System

  • Same integer-based cached logging level as the gateway, stored per-device in @Field static ConcurrentHashMap loggingLevelCache.

  • All logger() calls updated to use closures for lazy message evaluation.

Performance: Pre-Compiled Regex Patterns

  • All inline regex patterns in the sensor's attributeUpdate() switch/case (e.g., ~/batt[1-8]/, ~/humidity_wf[1-8]/, ~/soilmoisture([1-9]|1[0-6])$/, etc.) replaced with @Field static final Pattern constants.

  • Compiled once at class load, reused on every data cycle.

Performance: Cached SimpleDateFormat

  • timeEpochToLocal() previously created a new SimpleDateFormat on every call. Now uses a @Field static final cached instance.

Performance: HTML Template Regex

  • The $\{...\} pattern used for HTML template variable substitution is now pre-compiled as @Field static final RE_HTML_VAR.

  • HTML template rendering also uses cachedString() instead of device.currentValue().

Feature: VPD Reporting Toggle

  • New per-sensor preference: "Report VPD" (default: off).

  • VPD data is only processed and stored when enabled, reducing events for users who don't need it.

Fix: VPD Unit Labels Were Swapped

  • Original code: metric β†’ "inHg", imperial β†’ "kPa" (backwards!).

  • Fixed to: metric β†’ "kPa", imperial β†’ "inHg" with proper conversion factor (Γ— 0.2953).

Feature: Soil AD Reporting Toggle

  • New per-sensor preference: "Report Soil AD" (default: off).

  • Raw millivolt soil AD readings are only stored when explicitly enabled.

  • Stale soilAD state is cleaned up when the preference is turned off.

Code Cleanup: Shared Helpers

  • initSetting(name, defaultValue, type) β€” replaces repeated if (settings.x == null) device.updateSetting(...) blocks.

  • settingEnabled(name, defaultValue) β€” init + return in one call.

  • updateDangerColor(danger, color, attribDanger, attribColor) β€” deduplicates the paired attributeUpdateString() calls used by heat index, simmer index, wind chill, UV, and AQI.

  • deleteStaleState(attribute) β€” replaces ~25 instances of if (device.currentValue(x) != null) device.deleteCurrentState(x).

Fix: devStatusIsError() Simplified

  • Changed from checking "<font style='color:red'>" to "color:red" for more robust matching.

Fix: isBundled Type Coercion

  • device.getDataValue("isBundled") returns a String. The original code assigned it to Boolean bundled, which in Groovy means any non-null string is truthy. Fixed to explicit == "true" comparison.

Fix: Missing Comma in Logger Call

  • logger('W' {"Unrecognized attribute..."}) was missing a comma β†’ logger('W', {"Unrecognized attribute..."}).

Fix: clearAllStates() Now Resets Caches

  • Now also calls invalidateCache(), invalidateMetricCache(), and initSensorFlags() to ensure a clean slate.

Fix: Typo

  • "sttributes" β†’ "attributes" in comment.

Backward Compatibility

  • Fully backward-compatible. No new capabilities, attributes, or child device types were added.

  • The new "Enabled Sensors" preference defaults to all sensors enabled, matching the original behavior.

  • The new logging level uses integer values (1–5) instead of strings, so after updating you'll need to re-select your logging level preference once (it defaults to 3 = Info).

  • The VPD and Soil AD toggle preferences only appear in the sensor device UI after the first data cycle that contains those readings.


Summary of Impact

Area Before After
device.currentValue() calls per cycle Dozens per sensor 1 on cache miss, 0 on hit
Sensor key dispatch (gateway) Sequential switch/case (~50 cases + regex) O(1) map lookup + gated regex
state.* writes per cycle (sensor) 4–8 persistent DB writes 0 (in-memory map)
Regex compilation (both drivers) Inline on every call Pre-compiled once at class load
parent.unitSystemIsMetric() calls per cycle 15–20 cross-device calls 1 on cache miss
Logging overhead at Info level String interpolation + comparison Integer comparison, closure not evaluated

The previous changes are on a tag here: GitHub - esimioni/temp-ecowitt-hubitat at pre-ai-performance-review Β· GitHub

The final drivers with all changes can be found here: GitHub - esimioni/temp-ecowitt-hubitat Β· GitHub

4 Likes

Thanks again for such a detailed list of changes you have worked on (you may want to consider the summary/detail option to keep the posts a little shorter - EDIT Thanks for adjusting your post).

While I still wouldn't expect the drivers to have a material impact on the overall hub performance (I believe that percentage of CPU time is a percentage of overall CPU usage, so relative to how much other activity is going on), I can appreciate the benefit to at least doing things like keeping the list of events low to reduce impacts elsewhere in Maker API and alike. I am swayed by some of your comments and the cleaner nature of some of the changes you have made.

So I do want to look at these and your earlier post at some point, although I should really prioritise the other changes I have on my list first. That is likely to still be some time away, unfortunately. I will keep you updated when I get back to working on the driver's again.

4 Likes

yes seems like a lot of work for not a lot of gain.. as mentioned that 55 percent is off whatever percent your cpu is.. so in my case below for 2 of my weather stations report its about 20 percent of 3.4 percent with is really only .6% of the cpu.

So, not necessarily related to the HE integration. Since we all are using Ecowitt, you guys might know the answer though.

I do not use the WS View app for viewing the stats very often. So, it has been a while since I opened it.

Opened it up to look at some of my graphs, and got the pop up message "invalid userid" as a "tip". Nothing was not working. Just get that message when I open the app, and/or when I switch back to the ecowitt dashboard from the Home, or WU Dashboard, or any of the others.

Doesn't seem to be affecting anything. Everything loads just fine.

Appears to be related to Ecowitt dot net login. But, that works just fine in a browser. There are no log in credentials for that stored in the app. So, not sure what is going on.

Pop Up Error

Haven’t seen that in the WSView Plus app on iOS, though I don’t have my weather station configured with ecowitt.net credentials, just the local upload to Hubitat.

PS: I can’t remember for the life of me why exactly I switched from WSView to WSView Plus sometime in the last few years. But reviewing their Apple App Store pages, it looks like only the Plus app is still getting updated.

That was kind of my point. There are no Ecowitt weather credentials on the app. You have to go to the website, then paste the MAC for it to start uploading. There is no where on the app that you can actually put in the credentials. (Hence the head scratching)

Also, I was referring to the plus app. I just got into the habit of calling it the WSView App. (Wasn't the non-plus deprecated?)