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.

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.

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.
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.
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.
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.
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.
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.
New per-sensor toggle preferences for VPD and Soil AD reporting.
VPD unit conversion fix β The original had metric/imperial labels swapped.
Regex patterns are now pre-compiled
Minor bug fixes and code cleanup across both drivers.
ecowitt_gateway.groovy)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.
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.
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.
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.
updated() runs (e.g., user changes Imperial β Metric), it iterates child devices and calls invalidateMetricCache() on each, ensuring stale cached values are cleared.dateutc Storagestate.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.sensorIsBundled() Signaturechannel parameter from sensorIsBundled(Integer id, Integer channel) β sensorIsBundled(Integer id).devStatusIsError()switch β return in attributeUpdate()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."end-od-data" β "end-of-data", "childs" β "child".ecowitt_sensor.groovy)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.
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).
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).
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.
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.
SimpleDateFormattimeEpochToLocal() previously created a new SimpleDateFormat on every call. Now uses a @Field static final cached instance.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().
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.
Original code: metric β "inHg", imperial β "kPa" (backwards!).
Fixed to: metric β "kPa", imperial β "inHg" with proper conversion factor (Γ 0.2953).
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.
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).
devStatusIsError() Simplified"<font style='color:red'>" to "color:red" for more robust matching.isBundled Type Coerciondevice.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.logger('W' {"Unrecognized attribute..."}) was missing a comma β logger('W', {"Unrecognized attribute..."}).clearAllStates() Now Resets CachesinvalidateCache(), invalidateMetricCache(), and initSensorFlags() to ensure a clean slate."sttributes" β "attributes" in comment.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.
| 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
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.
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.
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?)