Last active
April 21, 2026 21:55
-
-
Save arstorey/d8cb99cf46bb482b30c78e7f63498b34 to your computer and use it in GitHub Desktop.
Home Assistant automation blueprint for firmware-level soil moisture and temperature calibration for THIRDREALITY Smart Soil Moisture Sensor Gen2 (3RSM0347Z).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| blueprint: | |
| name: ZHA – THIRDREALITY Gen2 Calibration (Strict per-device isolation) | |
| description: > | |
| Firmware-level soil moisture and temperature calibration for | |
| THIRDREALITY Smart Soil Moisture Sensor Gen2 (3RSM0347Z). | |
| • Strict per-device isolation | |
| • No global entities | |
| • ZHA device selector | |
| • Write → read → verify | |
| • Retry-safe for sleepy battery devices | |
| • Run manually (recommended) to avoid accidental repeated writes | |
| domain: automation | |
| input: | |
| zigbee_device: | |
| name: Gen2 Soil Moisture Sensor | |
| description: Select the THIRDREALITY Gen2 sensor (ZHA) | |
| selector: | |
| device: | |
| integration: zha | |
| soil_offset: | |
| name: Soil Moisture Calibration (%) | |
| default: 0 | |
| selector: | |
| number: | |
| min: -30 | |
| max: 30 | |
| step: 1 | |
| unit_of_measurement: "%" | |
| temperature_offset: | |
| name: Temperature Calibration (centi-°C) | |
| description: 100 = +1.00 °C | |
| default: 0 | |
| selector: | |
| number: | |
| min: -500 | |
| max: 500 | |
| step: 10 | |
| unit_of_measurement: "c°C" | |
| retry_minutes: | |
| name: Retry interval (minutes) | |
| default: 5 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 30 | |
| step: 1 | |
| max_attempts: | |
| name: Maximum retry attempts | |
| default: 6 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 30 | |
| step: 1 | |
| mode: restart | |
| max_exceeded: silent | |
| # Intentionally empty: this is a maintenance/provisioning workflow. | |
| # You run it manually from the automation UI. | |
| trigger: [] | |
| variables: | |
| # Pull inputs into normal YAML variables first (NOT inside Jinja) | |
| device_id: !input zigbee_device | |
| soil_target: !input soil_offset | |
| temp_target: !input temperature_offset | |
| retry_delay: !input retry_minutes | |
| retry_limit: !input max_attempts | |
| # Now it's safe to use Jinja | |
| ieee: "{{ device_attr(device_id, 'identifiers')[0][1] | lower }}" | |
| action: | |
| - repeat: | |
| sequence: | |
| # ---- WRITE soil moisture calibration ---- | |
| - service: zha.set_zigbee_cluster_attribute | |
| data: | |
| ieee: "{{ ieee }}" | |
| cluster_id: 0x0408 | |
| attribute: 0xF001 | |
| value: "{{ soil_target | int }}" | |
| # ---- WRITE temperature calibration ---- | |
| - service: zha.set_zigbee_cluster_attribute | |
| data: | |
| ieee: "{{ ieee }}" | |
| cluster_id: 0x0402 | |
| attribute: 0xF001 | |
| value: "{{ temp_target | int }}" | |
| # ---- READ soil moisture calibration ---- | |
| - service: zha.get_zigbee_cluster_attribute | |
| data: | |
| ieee: "{{ ieee }}" | |
| cluster_id: 0x0408 | |
| attribute: 0xF001 | |
| # ---- READ temperature calibration ---- | |
| - service: zha.get_zigbee_cluster_attribute | |
| data: | |
| ieee: "{{ ieee }}" | |
| cluster_id: 0x0402 | |
| attribute: 0xF001 | |
| # ---- WAIT for response from THIS device (strict isolation) ---- | |
| - wait_for_trigger: | |
| - platform: event | |
| event_type: zha_event | |
| event_data: | |
| command: read_attributes_response | |
| timeout: | |
| minutes: "{{ retry_delay | int }}" | |
| continue_on_timeout: true | |
| # ---- If we got a response, verify it is from our device and matches either value ---- | |
| - variables: | |
| ok: >- | |
| {{ | |
| wait.trigger is not none | |
| and ( | |
| (wait.trigger.event.data.get('device_ieee') | default('') | lower) == ieee | |
| or | |
| (wait.trigger.event.data.get('ieee') | default('') | lower) == ieee | |
| ) | |
| and ( | |
| ( | |
| wait.trigger.event.data.get('cluster_id') == 0x0408 and | |
| wait.trigger.event.data.get('attribute_id') == 0xF001 and | |
| (wait.trigger.event.data.get('value') | int) == (soil_target | int) | |
| ) | |
| or | |
| ( | |
| wait.trigger.event.data.get('cluster_id') == 0x0402 and | |
| wait.trigger.event.data.get('attribute_id') == 0xF001 and | |
| (wait.trigger.event.data.get('value') | int) == (temp_target | int) | |
| ) | |
| ) | |
| }} | |
| until: | |
| - condition: or | |
| conditions: | |
| # success | |
| - condition: template | |
| value_template: "{{ ok }}" | |
| # fail-safe | |
| - condition: template | |
| value_template: "{{ repeat.index >= (retry_limit | int) }}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment