[ipobserver] Add support for WiFi version and push based method. (#12151)

* Add support for Wifi version of ipObserver.
* make config private again.
* Add logging.
* Remove tags

Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Matthew Skinner 2022-02-12 19:34:16 +11:00 committed by GitHub
parent 1be50736e3
commit 35a6fdde60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 207 additions and 24 deletions

View File

@ -4,9 +4,14 @@ This binding is for any weather station that sends data to an IP Observer module
The weather stations that do this are made by a company in China called `Fine Offset` and then re-branded by many distribution companies around the world. The weather stations that do this are made by a company in China called `Fine Offset` and then re-branded by many distribution companies around the world.
Some of the brands include Aercus (433mhz), Ambient Weather (915mhz), Frogitt, Misol (433mhz), Pantech (433mhz), Sainlogic and many more. Some of the brands include Aercus (433mhz), Ambient Weather (915mhz), Frogitt, Misol (433mhz), Pantech (433mhz), Sainlogic and many more.
Whilst Ambient Weather has it own cloud based binding, the other brands will not work with that binding and Ambient Weather do not sell outside of the United States. Whilst Ambient Weather has it own cloud based binding, the other brands will not work with that binding and Ambient Weather do not sell outside of the United States.
This binding works fully offline and uses local scraping of the weather station data at 12 second resolution if you wish and is easy to setup.
This binding works fully offline and can work via one of two methods:
1. Local scraping of the weather station's `livedata` webpage at 12 second resolution (non WiFi models only).
2. Both WiFi and RJ45 models can be setup to push the data directly to the openHAB (default 8080) server directly and the binding can parse the data from the weather underground data.
The other binding worth mentioning is the weather underground binding that allows the data to be intercepted on its way to WU, however many of the weather stations do not allow the redirection of the WU data and require you to know how to do redirections with a custom DNS server on your network. The other binding worth mentioning is the weather underground binding that allows the data to be intercepted on its way to WU, however many of the weather stations do not allow the redirection of the WU data and require you to know how to do redirections with a custom DNS server on your network.
This binding is by far the easiest method and works for all the brands and will not stop the data still being sent to WU if you wish to do both at the same time. This binding with method 1 and a RJ45 model is by far the easiest method and works for all the brands and will not stop the data still being sent to WU if you wish to do both at the same time.
If your weather station came with a LCD screen instead of the IP Observer, you can add on the unit and the LCD screen will still work in parallel as the RF data is sent 1 way from the outdoor unit to the inside screens and IP Observer units. If your weather station came with a LCD screen instead of the IP Observer, you can add on the unit and the LCD screen will still work in parallel as the RF data is sent 1 way from the outdoor unit to the inside screens and IP Observer units.
## Supported Things ## Supported Things
@ -15,15 +20,21 @@ There is only one thing that can be added and is called `weatherstation`.
## Discovery ## Discovery
Auto discovery is supported and may take a while to complete as it scans all IP addresses on your network one by one. Auto discovery is supported for the RJ45 models, while the WiFi IP Observer will need to be manually added.
Discovery may take a while to complete as it scans all IP addresses on your network one by one.
## Thing Configuration ## Thing Configuration
When the id and password are supplied, you need to set the custom WU path to `/weatherstation/updateweatherstation.php` and the port to be the same as openHAB (port 8080 by default).
If they are left blank, the binding will work in the scraping mode (RJ45 model only).
| Parameter | Required | Description | | Parameter | Required | Description |
|-|-|-| |-|-|-|
| `address` | Y | Hostname or IP for the IP Observer | | `address` | Y | Hostname or IP for the IP Observer |
| `pollTime` | Y | Time in seconds between each Scan of the livedata.htm from the IP Observer | | `pollTime` | Y | Time in seconds between each Scan of the livedata.htm from the IP Observer |
| `autoReboot` | Y | Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this feature allowing you to manually trigger or use a rule to handle the reboots. | | `autoReboot` | Y | Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this feature allowing you to manually trigger or use a rule to handle the reboots. |
| `id` | N | The weather underground's `station ID` that is setup in the ipobservers settings. |
| `password` | N | The weather underground's `station key` that is setup in the ipobservers settings. |
## Channels ## Channels

View File

@ -26,6 +26,7 @@ public class IpObserverBindingConstants {
public static final String BINDING_ID = "ipobserver"; public static final String BINDING_ID = "ipobserver";
public static final String REBOOT_URL = "/msgreboot.htm"; public static final String REBOOT_URL = "/msgreboot.htm";
public static final String LIVE_DATA_URL = "/livedata.htm"; public static final String LIVE_DATA_URL = "/livedata.htm";
public static final String SERVER_UPDATE_URL = "/weatherstation/updateweatherstation.php";
public static final String STATION_SETTINGS_URL = "/station.htm"; public static final String STATION_SETTINGS_URL = "/station.htm";
public static final int DISCOVERY_THREAD_POOL_SIZE = 15; public static final int DISCOVERY_THREAD_POOL_SIZE = 15;
@ -35,6 +36,8 @@ public class IpObserverBindingConstants {
// List of all Channel ids // List of all Channel ids
public static final String TEMP_INDOOR = "temperatureIndoor"; public static final String TEMP_INDOOR = "temperatureIndoor";
public static final String TEMP_OUTDOOR = "temperatureOutdoor"; public static final String TEMP_OUTDOOR = "temperatureOutdoor";
public static final String TEMP_WIND_CHILL = "temperatureWindChill";
public static final String TEMP_DEW_POINT = "temperatureDewPoint";
public static final String INDOOR_HUMIDITY = "humidityIndoor"; public static final String INDOOR_HUMIDITY = "humidityIndoor";
public static final String OUTDOOR_HUMIDITY = "humidityOutdoor"; public static final String OUTDOOR_HUMIDITY = "humidityOutdoor";
public static final String ABS_PRESSURE = "pressureAbsolute"; public static final String ABS_PRESSURE = "pressureAbsolute";

View File

@ -24,4 +24,6 @@ public class IpObserverConfiguration {
public String address = ""; public String address = "";
public int pollTime = 20; public int pollTime = 20;
public int autoReboot = 2000; public int autoReboot = 2000;
public String password = "";
public String id = "";
} }

View File

@ -19,6 +19,7 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -70,10 +71,12 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class IpObserverHandler extends BaseThingHandler { public class IpObserverHandler extends BaseThingHandler {
private final HttpClient httpClient; private final HttpClient httpClient;
private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
private final Logger logger = LoggerFactory.getLogger(IpObserverHandler.class); private final Logger logger = LoggerFactory.getLogger(IpObserverHandler.class);
private Map<String, ChannelHandler> channelHandlers = new HashMap<String, ChannelHandler>(); private Map<String, ChannelHandler> channelHandlers = new HashMap<String, ChannelHandler>();
private @Nullable ScheduledFuture<?> pollingFuture = null; private @Nullable ScheduledFuture<?> pollingFuture = null;
private IpObserverConfiguration config = new IpObserverConfiguration(); private IpObserverConfiguration config = new IpObserverConfiguration();
private String idPass = "";
// Config settings parsed from weather station. // Config settings parsed from weather station.
private boolean imperialTemperature = false; private boolean imperialTemperature = false;
private boolean imperialRain = false; private boolean imperialRain = false;
@ -135,9 +138,47 @@ public class IpObserverHandler extends BaseThingHandler {
} }
} }
public IpObserverHandler(Thing thing, HttpClient httpClient) { public IpObserverHandler(Thing thing, HttpClient httpClient, IpObserverUpdateReceiver UpdateReceiver) {
super(thing); super(thing);
this.httpClient = httpClient; this.httpClient = httpClient;
ipObserverUpdateReceiver = UpdateReceiver;
}
/**
* Takes a String of queries from the GET request made to the openHAB Jetty server and splits them
* into keys and values made up from the weather stations readings.
*
* @param update
*/
public void processServerQuery(String update) {
if (update.startsWith(idPass)) {
String matchedUpdate = update.substring(idPass.length() + 1, update.length());
logger.trace("Update received:{}", matchedUpdate);
updateState(LAST_UPDATED_TIME, new DateTimeType(ZonedDateTime.now()));
Map<String, String> mappedQuery = new HashMap<>();
String[] readings = matchedUpdate.split("&");
for (String pair : readings) {
int index = pair.indexOf("=");
if (index > 0) {
mappedQuery.put(pair.substring(0, index), pair.substring(index + 1, pair.length()));
}
}
handleServerReadings(mappedQuery);
}
}
public void handleServerReadings(Map<String, String> updates) {
Iterator<?> it = updates.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<?, ?> pair = (Map.Entry<?, ?>) it.next();
ChannelHandler localUpdater = channelHandlers.get(pair.getKey());
if (localUpdater != null) {
logger.trace("Found element {}, value is {}", pair.getKey(), pair.getValue());
localUpdater.processValue(pair.getValue().toString());
} else {
logger.trace("UNKNOWN element {}, value is {}", pair.getKey(), pair.getValue());
}
}
} }
@Override @Override
@ -244,6 +285,27 @@ public class IpObserverHandler extends BaseThingHandler {
} }
} }
private void setupServerChannels() {
createChannelHandler(WIND_DIRECTION, QuantityType.class, Units.DEGREE_ANGLE, "winddir");
createChannelHandler(INDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "indoorhumidity");
createChannelHandler(OUTDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "humidity");
createChannelHandler(TEMP_INDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "indoortempf");
createChannelHandler(TEMP_OUTDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "tempf");
createChannelHandler(TEMP_WIND_CHILL, QuantityType.class, ImperialUnits.FAHRENHEIT, "windchillf");
createChannelHandler(TEMP_DEW_POINT, QuantityType.class, ImperialUnits.FAHRENHEIT, "dewptf");
createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, ImperialUnits.INCH, "rainin");
createChannelHandler(DAILY_RAIN, QuantityType.class, ImperialUnits.INCH, "dailyrainin");
createChannelHandler(WEEKLY_RAIN, QuantityType.class, ImperialUnits.INCH, "weeklyrainin");
createChannelHandler(MONTHLY_RAIN, QuantityType.class, ImperialUnits.INCH, "monthlyrainin");
createChannelHandler(YEARLY_RAIN, QuantityType.class, ImperialUnits.INCH, "yearlyrainin");
createChannelHandler(UV_INDEX, DecimalType.class, SIUnits.CELSIUS, "UV");
createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windspeedmph");
createChannelHandler(WIND_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windgustmph");
createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.IRRADIANCE, "solarradiation");
createChannelHandler(REL_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "baromin");
createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "lowbatt");
}
private void setupChannels() { private void setupChannels() {
if (imperialTemperature) { if (imperialTemperature) {
logger.debug("Using imperial units of measurement for temperature."); logger.debug("Using imperial units of measurement for temperature.");
@ -332,12 +394,20 @@ public class IpObserverHandler extends BaseThingHandler {
@Override @Override
public void initialize() { public void initialize() {
config = getConfigAs(IpObserverConfiguration.class); config = getConfigAs(IpObserverConfiguration.class);
updateStatus(ThingStatus.UNKNOWN); if (!config.id.isBlank() && !config.password.isBlank()) {
pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS); updateStatus(ThingStatus.ONLINE);
idPass = "ID=" + config.id + "&PASSWORD=" + config.password;
setupServerChannels();
ipObserverUpdateReceiver.addStation(this);
} else {
updateStatus(ThingStatus.UNKNOWN);
pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
}
} }
@Override @Override
public void dispose() { public void dispose() {
ipObserverUpdateReceiver.removeStation(this);
channelHandlers.clear(); channelHandlers.clear();
ScheduledFuture<?> localFuture = pollingFuture; ScheduledFuture<?> localFuture = pollingFuture;
if (localFuture != null) { if (localFuture != null) {

View File

@ -28,6 +28,7 @@ import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
/** /**
* The {@link IpObserverHandlerFactory} is responsible for creating things and thing * The {@link IpObserverHandlerFactory} is responsible for creating things and thing
@ -39,11 +40,14 @@ import org.osgi.service.component.annotations.Reference;
@Component(configurationPid = "binding.ipobserver", service = ThingHandlerFactory.class) @Component(configurationPid = "binding.ipobserver", service = ThingHandlerFactory.class)
public class IpObserverHandlerFactory extends BaseThingHandlerFactory { public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_WEATHER_STATION); private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_WEATHER_STATION);
private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
protected final HttpClient httpClient; protected final HttpClient httpClient;
@Activate @Activate
public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory) { public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference HttpService httpService) {
this.httpClient = httpClientFactory.getCommonHttpClient(); this.httpClient = httpClientFactory.getCommonHttpClient();
ipObserverUpdateReceiver = new IpObserverUpdateReceiver(httpService);
} }
protected HttpClient getHttpClient() { protected HttpClient getHttpClient() {
@ -60,7 +64,7 @@ public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_WEATHER_STATION.equals(thingTypeUID)) { if (THING_WEATHER_STATION.equals(thingTypeUID)) {
return new IpObserverHandler(thing, httpClient); return new IpObserverHandler(thing, httpClient, ipObserverUpdateReceiver);
} }
return null; return null;

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.ipobserver.internal;
import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.SERVER_UPDATE_URL;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link IpObserverUpdateReceiver} captures any updates sent to the openHAB Jetty server if the weather station is
* setup to direct the weather updates to the HTTP server of openHAB which is normally port 8080.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class IpObserverUpdateReceiver extends HttpServlet {
private static final long serialVersionUID = -234658674L;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private List<IpObserverHandler> listOfHandlers = new ArrayList<>(1);
public IpObserverUpdateReceiver(HttpService httpService) {
try {
httpService.registerServlet(SERVER_UPDATE_URL, this, null, httpService.createDefaultHttpContext());
} catch (NamespaceException | ServletException e) {
logger.warn("Registering servlet failed:{}", e.getMessage());
}
}
@Override
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
if (req == null) {
return;
}
String stationUpdate = req.getQueryString();
if (stationUpdate == null) {
return;
}
logger.debug("Weather station packet received from {}", req.getRemoteHost());
for (IpObserverHandler ipObserverHandler : listOfHandlers) {
ipObserverHandler.processServerQuery(stationUpdate);
}
}
public void addStation(IpObserverHandler ipObserverHandler) {
listOfHandlers.add(ipObserverHandler);
}
public void removeStation(IpObserverHandler ipObserverHandler) {
listOfHandlers.remove(ipObserverHandler);
}
}

View File

@ -14,6 +14,10 @@ thing-type.config.ipobserver.weatherstation.address.label = Network Address
thing-type.config.ipobserver.weatherstation.address.description = Hostname or IP for the IP Observer thing-type.config.ipobserver.weatherstation.address.description = Hostname or IP for the IP Observer
thing-type.config.ipobserver.weatherstation.autoReboot.label = Auto Reboot thing-type.config.ipobserver.weatherstation.autoReboot.label = Auto Reboot
thing-type.config.ipobserver.weatherstation.autoReboot.description = Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this feature allowing you to manually trigger or use a rule to handle the reboots thing-type.config.ipobserver.weatherstation.autoReboot.description = Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this feature allowing you to manually trigger or use a rule to handle the reboots
thing-type.config.ipobserver.weatherstation.id.label = Station ID
thing-type.config.ipobserver.weatherstation.id.description = The station ID used to connect to WeatherUnderGround. Leave blank if you wish to poll the livedata.
thing-type.config.ipobserver.weatherstation.password.label = Station Password
thing-type.config.ipobserver.weatherstation.password.description = The station password used to connect to WeatherUnderGround. Leave blank if you wish to poll the livedata.
thing-type.config.ipobserver.weatherstation.pollTime.label = Poll Time thing-type.config.ipobserver.weatherstation.pollTime.label = Poll Time
thing-type.config.ipobserver.weatherstation.pollTime.description = Time in seconds between each Scan of the livedata.htm from the ObserverIP thing-type.config.ipobserver.weatherstation.pollTime.description = Time in seconds between each Scan of the livedata.htm from the ObserverIP
@ -41,8 +45,12 @@ channel-type.ipobserver.responseTime.label = Response Time
channel-type.ipobserver.responseTime.description = How many milliseconds it took to fetch the sensor readings from livedata.htm channel-type.ipobserver.responseTime.description = How many milliseconds it took to fetch the sensor readings from livedata.htm
channel-type.ipobserver.solarRadiation.label = Solar Radiation channel-type.ipobserver.solarRadiation.label = Solar Radiation
channel-type.ipobserver.solarRadiation.description = Solar Radiation channel-type.ipobserver.solarRadiation.description = Solar Radiation
channel-type.ipobserver.temperatureDewPoint.label = Dew Point Temperature
channel-type.ipobserver.temperatureDewPoint.description = Dew Point Temperature Outdoors
channel-type.ipobserver.temperatureIndoor.label = Indoor Temperature channel-type.ipobserver.temperatureIndoor.label = Indoor Temperature
channel-type.ipobserver.temperatureIndoor.description = Current Temperature Indoors channel-type.ipobserver.temperatureIndoor.description = Current Temperature Indoors
channel-type.ipobserver.temperatureWindChill.label = Wind Chill Temperature
channel-type.ipobserver.temperatureWindChill.description = Wind Chill Temperature Outdoors
channel-type.ipobserver.uv.label = UV channel-type.ipobserver.uv.label = UV
channel-type.ipobserver.uv.description = UV channel-type.ipobserver.uv.description = UV
channel-type.ipobserver.uvIndex.label = UV Index channel-type.ipobserver.uvIndex.label = UV Index

View File

@ -10,6 +10,8 @@
<channels> <channels>
<channel id="temperatureIndoor" typeId="temperatureIndoor"/> <channel id="temperatureIndoor" typeId="temperatureIndoor"/>
<channel id="temperatureOutdoor" typeId="system.outdoor-temperature"/> <channel id="temperatureOutdoor" typeId="system.outdoor-temperature"/>
<channel id="temperatureWindChill" typeId="temperatureWindChill"/>
<channel id="temperatureDewPoint" typeId="temperatureDewPoint"/>
<channel id="humidityIndoor" typeId="humidityIndoor"/> <channel id="humidityIndoor" typeId="humidityIndoor"/>
<channel id="humidityOutdoor" typeId="system.atmospheric-humidity"/> <channel id="humidityOutdoor" typeId="system.atmospheric-humidity"/>
<channel id="pressureAbsolute" typeId="pressureAbsolute"/> <channel id="pressureAbsolute" typeId="pressureAbsolute"/>
@ -51,6 +53,16 @@
feature allowing you to manually trigger or use a rule to handle the reboots</description> feature allowing you to manually trigger or use a rule to handle the reboots</description>
<default>2000</default> <default>2000</default>
</parameter> </parameter>
<parameter name="id" type="text">
<label>Station ID</label>
<description>The station ID used to connect to WeatherUnderGround. Leave blank if you wish to poll the livedata.</description>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<label>Station Password</label>
<description>The station password used to connect to WeatherUnderGround. Leave blank if you wish to poll the
livedata.</description>
</parameter>
</config-description> </config-description>
</thing-type> </thing-type>
<channel-type id="responseTime" advanced="true"> <channel-type id="responseTime" advanced="true">
@ -70,6 +82,20 @@
</tags> </tags>
<state pattern="%.1f %unit%" readOnly="true"/> <state pattern="%.1f %unit%" readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="temperatureWindChill" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Wind Chill Temperature</label>
<description>Wind Chill Temperature Outdoors</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="temperatureDewPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Dew Point Temperature</label>
<description>Dew Point Temperature Outdoors</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="humidityIndoor"> <channel-type id="humidityIndoor">
<item-type>Number:Dimensionless</item-type> <item-type>Number:Dimensionless</item-type>
<label>Indoor Humidity</label> <label>Indoor Humidity</label>
@ -174,10 +200,6 @@
<label>Wind Max Gust</label> <label>Wind Max Gust</label>
<description>Max wind gust for today</description> <description>Max wind gust for today</description>
<category>Wind</category> <category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/> <state pattern="%.1f %unit%" readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="rainHourlyRate"> <channel-type id="rainHourlyRate">
@ -207,10 +229,6 @@
<label>Rain for Week</label> <label>Rain for Week</label>
<description>Weekly Rain</description> <description>Weekly Rain</description>
<category>Rain</category> <category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/> <state pattern="%.2f %unit%" readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="rainForMonth" advanced="true"> <channel-type id="rainForMonth" advanced="true">
@ -218,10 +236,6 @@
<label>Rain for Month</label> <label>Rain for Month</label>
<description>Rain since 12:00 on the 1st of this month</description> <description>Rain since 12:00 on the 1st of this month</description>
<category>Rain</category> <category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/> <state pattern="%.2f %unit%" readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="rainForYear"> <channel-type id="rainForYear">
@ -229,10 +243,6 @@
<label>Rain for Year</label> <label>Rain for Year</label>
<description>Total rain since 12:00 on 1st Jan</description> <description>Total rain since 12:00 on 1st Jan</description>
<category>Rain</category> <category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/> <state pattern="%.2f %unit%" readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="lastUpdatedTime" advanced="true"> <channel-type id="lastUpdatedTime" advanced="true">