~hww3/org.openhab.binding.weatherflowsmartweather

4ce577f28dba6752a4ebbd65a287167000611454 — H. William Welliver III 9 months ago afa7929
Add start of "grey market" air quality sensor
M src/main/java/org/openhab/binding/weatherflowsmartweather/WeatherFlowSmartWeatherBindingConstants.java => src/main/java/org/openhab/binding/weatherflowsmartweather/WeatherFlowSmartWeatherBindingConstants.java +15 -0
@@ 33,6 33,7 @@ public class WeatherFlowSmartWeatherBindingConstants {
    public static final ThingTypeUID THING_TYPE_SMART_WEATHER_AIR = new ThingTypeUID(BINDING_ID, "air");
    public static final ThingTypeUID THING_TYPE_SMART_WEATHER_SKY = new ThingTypeUID(BINDING_ID, "sky");
    public static final ThingTypeUID THING_TYPE_SMART_WEATHER_TEMPEST = new ThingTypeUID(BINDING_ID, "tempest");
    public static final ThingTypeUID THING_TYPE_SMART_WEATHER_AIRQUALITY = new ThingTypeUID(BINDING_ID, "quality");
    public static final ThingTypeUID THING_TYPE_SMART_WEATHER_BETTER_FORECAST = new ThingTypeUID(BINDING_ID,
            "better-forecast");



@@ 70,6 71,19 @@ public class WeatherFlowSmartWeatherBindingConstants {
    public static final String CHANNEL_FORECAST_RAW = "forecast_raw";
    public static final String CHANNEL_FORECAST_ENRICHED = "forecast_enriched";

    // Air quality channel ids
    public static final String CHANNEL_PM10 = "pm10";
    public static final String CHANNEL_PM25 = "pm25";
    public static final String CHANNEL_PM100 = "pm100";
    public static final String CHANNEL_PARTICLE_3UM = "particles_03um";
    public static final String CHANNEL_PARTICLE_5UM = "particles_05um";
    public static final String CHANNEL_PARTICLE_10UM = "particles_10um";
    public static final String CHANNEL_PARTICLE_25UM = "particles_25um";
    public static final String CHANNEL_PARTICLE_50UM = "particles_50um";
    public static final String CHANNEL_PARTICLE_100UM = "particles_100um";

    public static final String SKIP = "SKIP";

    // Event Channel ids
    public static final String CHANNEL_STRIKE_EVENTS = "strike_events";
    public static final String CHANNEL_RAPID_WIND_EVENTS = "rapid_wind_events";


@@ 95,6 109,7 @@ public class WeatherFlowSmartWeatherBindingConstants {
        thingTypes.add(THING_TYPE_SMART_WEATHER_SKY);
        thingTypes.add(THING_TYPE_SMART_WEATHER_TEMPEST);
        thingTypes.add(THING_TYPE_SMART_WEATHER_BETTER_FORECAST);
        thingTypes.add(THING_TYPE_SMART_WEATHER_AIRQUALITY);
        SUPPORTED_THING_TYPES = Set.copyOf(thingTypes);
    }
    public static final String PROPERTY_SERIAL_NUMBER = "serial_number";

A src/main/java/org/openhab/binding/weatherflowsmartweather/handler/SmartWeatherAirQualityHandler.java => src/main/java/org/openhab/binding/weatherflowsmartweather/handler/SmartWeatherAirQualityHandler.java +203 -0
@@ 0,0 1,203 @@
/**
 * Copyright (c) 2014,2017 by the respective copyright holders.
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.weatherflowsmartweather.handler;

import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.weatherflowsmartweather.WeatherFlowSmartWeatherBindingConstants.*;

import java.net.InetAddress;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.measure.quantity.*;

import org.openhab.binding.weatherflowsmartweather.SmartWeatherEventListener;
import org.openhab.binding.weatherflowsmartweather.model.*;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;

/**
 * The {@link SmartWeatherAirQualityHandler} is responsible for handling commands, which are
 * sent to one of the channels.
 *
 * @author William Welliver - Initial contribution
 */

public class SmartWeatherAirQualityHandler extends BaseThingHandler implements SmartWeatherEventListener {

    private final Logger logger = LoggerFactory.getLogger(SmartWeatherAirQualityHandler.class);

    private Gson gson = new Gson();
    private ScheduledFuture<?> messageTimeout;

    private EventPublisher eventPublisher;

    public SmartWeatherAirQualityHandler(Thing thing, EventPublisher eventPublisher) {
        super(thing);
        this.eventPublisher = eventPublisher;
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        // if (channelUID.getId().equals(CHANNEL_1)) {
        // TODO: handle command

        // Note: if communication with thing fails for some reason,
        // indicate that by setting the status with detail information
        // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
        // "Could not control device at IP address x.x.x.x");
        // }
    }

    @Override
    public void initialize() {
        // TODO: Initialize the thing. If done set status to ONLINE to indicate proper working.
        // Long running initialization should be done asynchronously in background.
        updateStatus(ThingStatus.ONLINE);

        // Note: When initialization can NOT be done set the status with more details for further
        // analysis. See also class ThingStatusDetail for all available status details.
        // Add a description to give user information to understand why thing does not work
        // as expected. E.g.
        // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
        // "Can not access device as username and/or password are invalid");
    }

    // wonder if perhaps the refresh rate on this data may be too high by default... do we really need to
    // update this information multiple times a minute?

    @Override
    public void eventReceived(InetAddress source, SmartWeatherMessage data) {
        if (data instanceof StationStatusMessage || data instanceof DeviceStatusMessage) {
            logger.debug("got status message message: " + data);

            if (messageTimeout != null) {
                messageTimeout.cancel(true);
            }
            if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
                goOnline();
            }
            messageTimeout = scheduler.schedule(new Runnable() {
                @Override
                public void run() {
                    goOffline();
                }
            }, 3, TimeUnit.MINUTES);

            // TODO update station status fields
        } else if (data instanceof ObservationAirQualityMessage) {
            // EventStrikeMessage m = new EventStrikeMessage();
            // m.setSerial_number(((ObservationAirMessage) data).getSerial_number());
            // m.setFirmware_revision(((ObservationAirMessage) data).getFirmware_revision());
            // m.setHub_sn(((ObservationAirMessage) data).getHub_sn());
            // m.setSerial_number(data.getSerial_number());
            // m.setEvt(new Object[] { 0.0d, 1.0d, 1.2d });
            // logger.debug("Handling Strike Event");
            // handleEventStrikeMessage(m);
            handleObservationMessage((ObservationAirQualityMessage) data);
        } else {
            logger.warn("not handling message: " + data);
        }
    }

    public void handleObservationMessage(ObservationAirQualityMessage data) {
        // logger.warn("Received observation message: " + data);
        List<List> l = data.getObs();
        ThingUID uid = getThing().getUID();

        for (List obs : l) {
            // logger.info("parsing observation record: " + obs);

            String[] fields = { CHANNEL_EPOCH, SKIP, SKIP, SKIP, CHANNEL_PM10, CHANNEL_PM25, CHANNEL_PM100,
                    CHANNEL_PARTICLE_3UM, CHANNEL_PARTICLE_5UM, CHANNEL_PARTICLE_10UM, CHANNEL_PARTICLE_25UM,
                    CHANNEL_PARTICLE_50UM, CHANNEL_PARTICLE_100UM, SKIP, CHANNEL_BATTERY_LEVEL };
            int i = 0;
            for (String f : fields) {
                Double val = (Double) obs.get(i++);
                State type = null;
                switch (f) {
                    case SKIP:
                        continue;
                    case CHANNEL_EPOCH:
                        type = new DateTimeType(Instant.ofEpochMilli(val.longValue() * 1000).atZone(UTC));
                        break;
                    case CHANNEL_PM10:
                        type = new QuantityType<>(val, Units.MICROGRAM_PER_CUBICMETRE);
                        break;
                    case CHANNEL_PM25:
                        type = new QuantityType<>(val, Units.MICROGRAM_PER_CUBICMETRE);
                        break;
                    case CHANNEL_PM100:
                        type = new QuantityType<>(val, Units.MICROGRAM_PER_CUBICMETRE);
                        break;
                    case CHANNEL_PARTICLE_3UM:
                        type = new DecimalType(val);
                        break;
                    case CHANNEL_PARTICLE_5UM:
                        type = new DecimalType(val);
                        break;
                    case CHANNEL_PARTICLE_10UM:
                        type = new DecimalType(val);
                        break;
                    case CHANNEL_PARTICLE_25UM:
                        type = new DecimalType(val);
                        break;
                    case CHANNEL_PARTICLE_50UM:
                        type = new DecimalType(val);
                        break;
                    case CHANNEL_PARTICLE_100UM:
                        type = new DecimalType(val);
                        break;
                    case CHANNEL_BATTERY_LEVEL:
                        type = new QuantityType<ElectricPotential>(val, Units.VOLT);
                        break;
                    default:
                        logger.info("Received unknown field " + f + " with value " + val);
                }

                if (type != null) {
                    logger.debug("posting type = " + type);
                    updateState(new ChannelUID(uid, f), type);
                } else {
                    logger.warn("passed through without a type to update.");
                }
            }
        }
    }

    private void goOnline() {
        this.updateStatus(ThingStatus.ONLINE);
        messageTimeout = null;
    }

    protected void goOffline() {
        this.updateStatus(ThingStatus.OFFLINE);
        messageTimeout = null;
    }
}

M src/main/java/org/openhab/binding/weatherflowsmartweather/handler/SmartWeatherHubHandler.java => src/main/java/org/openhab/binding/weatherflowsmartweather/handler/SmartWeatherHubHandler.java +16 -0
@@ 131,6 131,20 @@ public class SmartWeatherHubHandler extends BaseBridgeHandler implements SmartWe
                } // not our hub and sensor combo.
                SmartWeatherEventListener handler = (SmartWeatherEventListener) t.getHandler();
                handler.eventReceived(source, message);
            } else if (data instanceof ObservationAirQualityMessage) {
                ObservationAirQualityMessage message = (ObservationAirQualityMessage) data;
                String serialNumber = message.getSerial_number();

                ThingTypeUID deviceType = thingTypeUidFromSerial(serialNumber);
                ThingUID thingUid = new ThingUID(deviceType, getThing().getUID(), serialNumber);

                Thing t = this.getThingByUID(thingUid);
                if (t == null) {
                    logger.debug("airquality observation but not for us.");
                    return;
                } // not our hub and sensor combo.
                SmartWeatherEventListener handler = (SmartWeatherEventListener) t.getHandler();
                handler.eventReceived(source, message);
            } else if (data instanceof ObservationAirMessage) {
                ObservationAirMessage message = (ObservationAirMessage) data;
                String serialNumber = message.getSerial_number();


@@ 238,6 252,8 @@ public class SmartWeatherHubHandler extends BaseBridgeHandler implements SmartWe
            return THING_TYPE_SMART_WEATHER_SKY;
        else if (serialNumber.startsWith("AR"))
            return THING_TYPE_SMART_WEATHER_AIR;
        else if (serialNumber.startsWith("AQ"))
            return THING_TYPE_SMART_WEATHER_AIRQUALITY;
        else if (serialNumber.startsWith("ST"))
            return THING_TYPE_SMART_WEATHER_TEMPEST;
        return null;

M src/main/java/org/openhab/binding/weatherflowsmartweather/internal/SmartWeatherDiscoveryService.java => src/main/java/org/openhab/binding/weatherflowsmartweather/internal/SmartWeatherDiscoveryService.java +2 -1
@@ 9,6 9,7 @@ import java.util.Set;
import org.openhab.binding.weatherflowsmartweather.SmartWeatherEventListener;
import org.openhab.binding.weatherflowsmartweather.WeatherFlowSmartWeatherBindingConstants;
import org.openhab.binding.weatherflowsmartweather.model.HubStatusMessage;
import org.openhab.binding.weatherflowsmartweather.model.HubStatusV30Message;
import org.openhab.binding.weatherflowsmartweather.model.SmartWeatherMessage;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;


@@ 66,7 67,7 @@ public class SmartWeatherDiscoveryService extends AbstractDiscoveryService imple

    @Override
    public void eventReceived(InetAddress source, SmartWeatherMessage data) {
        if (data instanceof HubStatusMessage) {
        if (data instanceof HubStatusMessage || data instanceof HubStatusV30Message) {
            HubStatusMessage message = (HubStatusMessage) data;
            String serial = message.getSerial_number();


M src/main/java/org/openhab/binding/weatherflowsmartweather/internal/SmartWeatherStationDiscoveryService.java => src/main/java/org/openhab/binding/weatherflowsmartweather/internal/SmartWeatherStationDiscoveryService.java +5 -1
@@ 90,7 90,11 @@ public class SmartWeatherStationDiscoveryService extends AbstractDiscoveryServic
            return;
        }

        if (serial != null && serial.startsWith("AR")) {
        if (serial != null && serial.startsWith("AQ")) {
            // we have an Air Quality sensor.
            label = "SmartWeather Air Quality";
            thingType = WeatherFlowSmartWeatherBindingConstants.THING_TYPE_SMART_WEATHER_AIRQUALITY;
        } else if (serial != null && serial.startsWith("AR")) {
            // we have an AIR sensor.
            label = "SmartWeather Air";
            thingType = WeatherFlowSmartWeatherBindingConstants.THING_TYPE_SMART_WEATHER_AIR;

M src/main/java/org/openhab/binding/weatherflowsmartweather/internal/WeatherFlowSmartWeatherHandlerFactory.java => src/main/java/org/openhab/binding/weatherflowsmartweather/internal/WeatherFlowSmartWeatherHandlerFactory.java +8 -2
@@ 117,7 117,7 @@ public class WeatherFlowSmartWeatherHandlerFactory extends BaseThingHandlerFacto

    @Override
    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
        // logger.warn("SupportsThingType: " + thingTypeUID + "? " + SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID));
        logger.warn("? SupportsThingType: " + thingTypeUID + "? " + SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID));
        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
    }



@@ 125,12 125,14 @@ public class WeatherFlowSmartWeatherHandlerFactory extends BaseThingHandlerFacto
    protected @Nullable ThingHandler createHandler(Thing thing) {
        ThingTypeUID thingTypeUID = thing.getThingTypeUID();

        logger.debug("Creating handler for thing=" + thingTypeUID);
        logger.warn("Creating handler for thing=" + thingTypeUID);

        if (thingTypeUID.equals(THING_TYPE_SMART_WEATHER_HUB)) {
            SmartWeatherHubHandler hubHandler = new SmartWeatherHubHandler((Bridge) thing, udpListener);
            registerDeviceDiscoveryService(hubHandler);
            return hubHandler;
        } else if (thingTypeUID.equals(THING_TYPE_SMART_WEATHER_AIRQUALITY)) {
            return new SmartWeatherAirQualityHandler(thing, eventPublisher);
        } else if (thingTypeUID.equals(THING_TYPE_SMART_WEATHER_AIR)) {
            return new SmartWeatherAirHandler(thing, lightningStrikeEventFactory, eventPublisher);
        } else if (thingTypeUID.equals(THING_TYPE_SMART_WEATHER_SKY)) {


@@ 149,12 151,16 @@ public class WeatherFlowSmartWeatherHandlerFactory extends BaseThingHandlerFacto

    @Override
    protected @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, ThingUID thingUID) {
        logger.warn("Creating thing for thing=" + thingTypeUID);
        logger.warn("ThingType: " + getThingTypeByUID(thingTypeUID));
        return super.createThing(thingTypeUID, configuration, thingUID);
    }

    @Override
    public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
            @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
        logger.warn("Creating thing for thing=" + thingTypeUID);
        logger.warn("ThingType: " + getThingTypeByUID(thingTypeUID));
        return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
    }


A src/main/java/org/openhab/binding/weatherflowsmartweather/model/ObservationAirQualityMessage.java => src/main/java/org/openhab/binding/weatherflowsmartweather/model/ObservationAirQualityMessage.java +49 -0
@@ 0,0 1,49 @@
package org.openhab.binding.weatherflowsmartweather.model;

import java.util.List;

public class ObservationAirQualityMessage extends SmartWeatherMessage {
    private String hub_sn;

    private List<List> obs;

    private int firmware_revision;

    public int getFirmware_revision() {
        return firmware_revision;
    }

    public void setFirmware_revision(int firmware_revision) {
        this.firmware_revision = firmware_revision;
    }

    public String getHub_sn() {
        return hub_sn;
    }

    public void setHub_sn(String hub_sn) {
        this.hub_sn = hub_sn;
    }
    //
    // public int getDevice_id() {
    // return device_id;
    // }
    //
    // public void setDevice_id(int device_id) {
    // this.device_id = device_id;
    // }

    public List<List> getObs() {
        return obs;
    }

    public void setObs(List<List> obs) {
        this.obs = obs;
    }

    @Override
    public String toString() {
        return "ObservationAirQualityMessage{" + "hub_sn='" + hub_sn + '\'' + ", obs=" + obs + ", firmware_revision="
                + firmware_revision + ", serial_number='" + serial_number + '\'' + '}';
    }
}

M src/main/java/org/openhab/binding/weatherflowsmartweather/model/SmartWeatherDeserializer.java => src/main/java/org/openhab/binding/weatherflowsmartweather/model/SmartWeatherDeserializer.java +2 -0
@@ 24,6 24,8 @@ public class SmartWeatherDeserializer implements JsonDeserializer<SmartWeatherMe
        Gson gson = new Gson();
        SmartWeatherMessage message = null;
        switch (messageType) {
            case "obs_pm":
                return gson.fromJson(je, ObservationAirQualityMessage.class);
            case "obs_air":
                return gson.fromJson(je, ObservationAirMessage.class);
            case "obs_sky":

M src/main/resources/OH-INF/thing/thing-types.xml => src/main/resources/OH-INF/thing/thing-types.xml +99 -0
@@ 27,6 27,33 @@
		</properties>
	</thing-type>

	<thing-type id="quality">
		<supported-bridge-type-refs>
			<bridge-type-ref id="hub"/>
		</supported-bridge-type-refs>

		<label>SmartWeather Air Quality</label>
		<description>Air quality sensors</description>

		<channels>
			<channel id="epoch" typeId="epoch"/>
			<channel id="pm10" typeId="pm10"/>
			<channel id="pm25" typeId="pm25"/>
			<channel id="pm100" typeId="pm100"/>
			<channel id="particles_03um" typeId="particles_03um"/>
			<channel id="particles_05um" typeId="particles_05um"/>
			<channel id="particles_10um" typeId="particles_10um"/>
			<channel id="particles_25um" typeId="particles_25um"/>
			<channel id="particles_50um" typeId="particles_50um"/>
			<channel id="particles_100um" typeId="particles_100um"/>
			<channel id="battery_level" typeId="battery_level"/>
		</channels>

		<properties>
			<property name="serial_number"></property>
		</properties>
	</thing-type>

	<thing-type id="sky">
		<supported-bridge-type-refs>
			<bridge-type-ref id="hub"/>


@@ 312,4 339,76 @@
		<category>Battery</category>
		<state pattern="%.1f %unit%" readOnly="true"/>
	</channel-type>

	<channel-type id="pm100">
		<item-type>Number</item-type>
		<label>PM10.0</label>
		<description>Particle Mass 10um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="pm25">
		<item-type>Number</item-type>
		<label>PM2.5</label>
		<description>Particle Mass 2.5um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="pm10">
		<item-type>Number</item-type>
		<label>PM1.0</label>
		<description>Particle Mass 1.0um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="particles_03um">
		<item-type>Number</item-type>
		<label>Particles 0.3um</label>
		<description>Particle count 0.3um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="particles_05um">
		<item-type>Number</item-type>
		<label>Particles 0.5um</label>
		<description>Particle count 0.5um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="particles_10um">
		<item-type>Number</item-type>
		<label>Particles 1.0um</label>
		<description>Particle count 1.0um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="particles_25um">
		<item-type>Number</item-type>
		<label>Particles 2.5um</label>
		<description>Particle count 2.5um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

	<channel-type id="particles_50um">
		<item-type>Number</item-type>
		<label>Particles 5um</label>
		<description>Particle count 5um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>
	<channel-type id="particles_100um">
		<item-type>Number</item-type>
		<label>Particles 10um</label>
		<description>Particle count 10um</description>
		<category>Number</category>
		<state readOnly="true"/>
	</channel-type>

</thing:thing-descriptions>