OzWrapper.OzWindV2.OzWindV2.OzWindV2

class OzWrapper.OzWindV2.OzWindV2.OzWindV2

Bases: SensorBase.SensorBase.GenericSensor

V2 wind sensor wrapper for Honguv ultrasonic anemometers.

Subclass of SensorBase.SensorBase.GenericSensor that wraps the drivers.Honguv.Honguv.Honguv driver and reports three derived quantities to the Oizom gateway:

  • Wind speed (m/s) - vector magnitude of the accumulated samples.

  • Wind direction (deg, 0-360) - vector-averaged bearing computed via trigonometric decomposition.

  • Gust - the instantaneous peak captured during the aggregation window (the raw ultrasonic samples preserve short bursts that a mechanical anemometer would smooth out).

Compared to v1 (OzWrapper.OzWind.OzWind.OzWind) this wrapper swaps the cup-and-vane reading model for the ultrasonic Honguv head and replaces scalar averaging with vector averaging.

Variables:
  • configuration (list[dict]) – Per-sensor configuration dicts from the gateway. Each entry carries the part number, enable flag, GPIO/UART settings, and the list of parameters to publish.

  • honguv (drivers.Honguv.Honguv.Honguv | None) – Honguv driver instance, or None before initialize() runs.

  • wind_angle (list[float]) – Accumulated wind direction samples in degrees, drained on every putSensorValue() call.

  • wind_speed (list[float]) – Accumulated wind speed samples in m/s, drained on every putSensorValue() call.

Example

>>> from OzWrapper.OzWindV2.OzWindV2 import OzWindV2
>>> wrapper = OzWindV2()
>>> wrapper.initialize(config_list, {})
>>> wrapper.getSensorReading()
>>> wrapper.putSensorValue({})

See also

SensorBase.SensorBase.GenericSensor: Base contract this wrapper implements (partNumber, parameter, baseConfig). OzWrapper.OzWind.OzWind.OzWind: V1 wrapper for the mechanical cup-and-vane sensor. drivers.Wind.Wind.Wind: V1 mechanical wind driver. drivers.Honguv.Honguv.Honguv: Underlying ultrasonic driver used by v2.

Initialise the OzWindV2 wrapper with empty state.

Invokes the SensorBase.SensorBase.GenericSensor constructor to pick up the base partNumber, parameter, and baseConfig descriptors, then prepares the empty buffers used by getSensorReading() and putSensorValue().

Parameters:

None.

Returns:

None. Instance attributes are mutated in place.

Raises:

Exception – Propagated from SensorBase.SensorBase.GenericSensor.__init__() if the base class fails to construct.

Example

>>> wrapper = OzWindV2()
>>> wrapper.honguv is None
True

Note

configuration is initialised to an empty dict here but is overwritten with the gateway’s config list inside initialize(); callers should not rely on the initial type.

getSensorReading()

Sample every initialised wind sensor and accumulate samples.

For each sensor whose init flag is 1 the method:

  1. Calls getWind() for every declared parameter.

  2. Applies the configured sensitivity (se as a percentage, divided by 100) and correction offset (cr, divided by 10) so the raw value matches the calibrated short-code value.

  3. Appends the corrected sample to wind_angle (pm == 1) or wind_speed (pm == 2) so putSensorValue() can compute the vector average on flush.

  4. Mirrors the latest corrected value into the returned dict so the gateway can show “instantaneous” telemetry alongside the aggregated payload.

Parameters:

None.

Returns:

Mapping of parameter short-code (sc) to the latest corrected reading taken in this cycle. Returns an empty dict when no sensors are initialised.

Return type:

dict

Raises:

Exception – Per-parameter exceptions are caught and logged; this method does not raise on read failures, only the affected short-code is skipped.

Example

>>> wrapper = OzWindV2()
>>> wrapper.getSensorReading()
{'wd': 137, 'ws': 4.2}

Note

Sensitivity/correction are applied to the rounded integer direction too, so callers that need the raw bearing should call getWind() directly instead of relying on this method’s output.

getWind(partNo: int, pm: int)

Read one wind quantity (direction or speed) from the Honguv head.

Dispatches on the part number and parameter id, calls drivers.Honguv.Honguv.Honguv.getWindData() (with the direction-only flag when only the bearing is needed), and rounds the direction to an integer to keep the cloud payload free of redundant float precision.

Parameters:
  • partNo (int) – Hardware part number. Only 214 (Honguv ultrasonic anemometer) is handled; any other value falls through and returns None implicitly.

  • pm (int) – Parameter identifier. 1 selects wind direction (degrees), 2 selects wind speed (m/s).

Returns:

Wind direction in degrees (int, 0-360) when pm == 1, wind speed in m/s (float) when pm == 2, otherwise None.

Return type:

int | float | None

Raises:

Exception – Propagated from drivers.Honguv.Honguv.Honguv.getWindData() on Modbus or parsing errors.

Example

>>> wrapper = OzWindV2()
>>> wrapper.getWind(214, 1)
123
>>> wrapper.getWind(214, 2)
3.7

Note

Wind direction is rounded with round() (not truncated) so that, for example, 359.6 becomes 360 rather than 359; downstream code can then safely apply a % 360 wrap if needed.

initialize(config: dict, init_value)

Initialise every Honguv wind sensor declared in the gateway config.

Stores the configuration list, iterates each entry, and delegates the actual hardware bring-up to initializeSensor(). Any per-sensor exception is caught and recorded as init = 0 so a single faulty unit does not abort the rest of the rig.

Parameters:
  • config (list[dict]) – List of sensor configuration dicts from the gateway. The argument is named config for parity with the base API even though it is iterated as a list.

  • init_value (dict) – Mutable dict updated in place by putInitvalues() with a windv2 key carrying the per-sensor init flags.

Returns:

True when at least one sensor was matched (the wrapper’s part number was present in some entry), otherwise False. The return value is treated as a “wrapper relevant for this rig” hint by the gateway loader.

Return type:

bool

Raises:

Exception – Per-sensor exceptions are caught and logged; this method itself does not raise.

Example

>>> wrapper = OzWindV2()
>>> status = {}
>>> wrapper.initialize(gateway_config, status)
True

Note

The wrapper only acts on entries whose en flag matches baseConfig["oz_enable"] and whose part number equals 214; all others are silently ignored.

initializeSensor(sensor: dict)

Initialise a single Honguv ultrasonic anemometer.

Builds a drivers.Honguv.Honguv.Honguv driver, opens the Modbus RTU port (defaulting to /dev/ttyAMA2 at 9600 baud when the config omits gpio settings), enables debug mode if requested, and primes each declared parameter with one initial reading so the gateway has a non-empty cache from the first cycle.

Parameters:

sensor (dict) – Single sensor configuration dict. Expected keys include the part-number field (must equal 214), en (must match baseConfig["oz_enable"]), parameters (list of parameter dicts), and an optional gpio sub-dict with port, baud, and debug.

Returns:

1 if the sensor was matched and the driver came up, -1 if the part number / enable flag didn’t match or any step failed.

Return type:

int

Raises:

Exception – Propagated from drivers.Honguv.Honguv.Honguv.initialize() or getWind() if Modbus communication fails; callers in initialize() catch these.

Example

>>> wrapper = OzWindV2()
>>> wrapper.initializeSensor(sensor_cfg)
1

Note

The “TODO: Add gpio configuration” marker in the body is a reminder that GPIO line pinning (e.g. RS-485 DE/RE control) is still handled by the kernel device tree rather than this wrapper.

putInitvalues(value: dict) dict

Record per-sensor init status under the windv2 key.

Walks the stored configuration list and copies each entry’s init flag (1 for success, -1 for skip/failure, 0 for caught exception) into a positional list, then attaches it to the gateway’s init-values dict so downstream code can distinguish the v2 wrapper from the v1 OzWrapper.OzWind.OzWind.OzWind results.

Parameters:

value (dict) – Mutable dict shared across all wrappers; this method adds (or overwrites) the windv2 key.

Returns:

The same dict, now containing value["windv2"] == [init_flag_for_sensor_0, ...].

Return type:

dict

Raises:

KeyError – If any configuration entry is missing its init flag (i.e. initialize() was not run).

Example

>>> wrapper = OzWindV2()
>>> wrapper.putInitvalues({})
{'windv2': [1]}

Note

The iteration variable X is unused on purpose; the index is kept around in case future revisions want to expose per-slot diagnostics alongside the bare init flag.

putSensorValue(result: dict) dict

Flush accumulated samples into vector-averaged wind values.

For every initialised sensor, writes the aggregated direction (vector_direction()) under the parameter’s pm == 1 short-code and the aggregated speed (vector_magnitude()) under the pm == 2 short-code. The internal wind_angle and wind_speed buffers are cleared at the end so the next aggregation window starts empty.

Parameters:

result (dict) – Mutable output dict in which to publish the averaged values. If None is passed a fresh dict is created and returned.

Returns:

The same dict (or the newly created one) with the pm == 1 / pm == 2 short-codes filled in.

Return type:

dict

Raises:

KeyError – If a parameter dict is missing the sc short-code key.

Example

>>> wrapper = OzWindV2()
>>> payload = wrapper.putSensorValue({})
>>> sorted(payload)
['wd', 'ws']

Note

This method is destructive: after it returns, the sample buffers are empty, so it must be called exactly once per aggregation window. Calling it twice in succession will yield zeros from vector_direction() and vector_magnitude().

vector_direction() float

Compute the vector-averaged wind direction from buffered samples.

Decomposes each accumulated direction sample into sine and cosine components weighted by the matching speed sample (so calm-period bearings have less influence than gust-period bearings), averages the components, and recovers the bearing via math.atan2(). Bearings are wrapped into the 0-360 range. A safety fallback to the arithmetic mean is used when the vector sum cancels exactly to zero.

Parameters:

self.wind_speed. (None. Reads from self.wind_angle and)

Returns:

Vector-averaged wind direction in degrees rounded to an int (declared as float for the base contract but actually integer-valued). Returns 0.0 when no samples have been buffered.

Return type:

float

Raises:

ValueError – From math.radians() / math.atan2() if a sample is non-numeric.

Example

>>> wrapper = OzWindV2()
>>> wrapper.wind_angle = [350, 10]
>>> wrapper.wind_speed = [3.0, 3.0]
>>> wrapper.vector_direction()
0

Note

The method iterates range(len(u)) with an explicit i >= len(v) break so that a mismatch between the two buffers (e.g. one parameter failing to read for a cycle) does not raise an IndexError.

vector_magnitude() float

Compute the vector-averaged wind speed from buffered samples.

Mirrors vector_direction() but returns the magnitude of the averaged sine/cosine vector rather than its bearing. The magnitude is always less than or equal to the arithmetic mean of the speeds because direction variability cancels part of the wind energy - this is the desired behaviour for reporting a “mean wind” alongside a “gust” peak.

Parameters:

self.wind_speed. (None. Reads from self.wind_angle and)

Returns:

Vector-averaged wind speed in m/s, rounded to two decimal places. Returns 0.0 when no samples have been buffered.

Return type:

float

Raises:

ValueError – From math.radians() / math.sqrt() if a sample is non-numeric or negative under the square root (the latter should be unreachable for real data).

Example

>>> wrapper = OzWindV2()
>>> wrapper.wind_angle = [90, 90]
>>> wrapper.wind_speed = [5.0, 5.0]
>>> wrapper.vector_magnitude()
5.0

Note

The buffers are not cleared here; clearing happens once per cycle inside putSensorValue() so that both vector_direction() and vector_magnitude() see the same samples.

baseConfig: ClassVar[dict]
configuration
honguv = None
parameter = 'pm'
partNumber = 'pn'
wind_angle = []
wind_speed = []