mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
1be50736e3
commit
35a6fdde60
@ -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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Supported Things
|
||||
@ -15,15 +20,21 @@ There is only one thing that can be added and is called `weatherstation`.
|
||||
|
||||
## 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
|
||||
|
||||
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 |
|
||||
|-|-|-|
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
|
@ -26,6 +26,7 @@ public class IpObserverBindingConstants {
|
||||
public static final String BINDING_ID = "ipobserver";
|
||||
public static final String REBOOT_URL = "/msgreboot.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 int DISCOVERY_THREAD_POOL_SIZE = 15;
|
||||
|
||||
@ -35,6 +36,8 @@ public class IpObserverBindingConstants {
|
||||
// List of all Channel ids
|
||||
public static final String TEMP_INDOOR = "temperatureIndoor";
|
||||
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 OUTDOOR_HUMIDITY = "humidityOutdoor";
|
||||
public static final String ABS_PRESSURE = "pressureAbsolute";
|
||||
|
@ -24,4 +24,6 @@ public class IpObserverConfiguration {
|
||||
public String address = "";
|
||||
public int pollTime = 20;
|
||||
public int autoReboot = 2000;
|
||||
public String password = "";
|
||||
public String id = "";
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@ -70,10 +71,12 @@ import org.slf4j.LoggerFactory;
|
||||
@NonNullByDefault
|
||||
public class IpObserverHandler extends BaseThingHandler {
|
||||
private final HttpClient httpClient;
|
||||
private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
|
||||
private final Logger logger = LoggerFactory.getLogger(IpObserverHandler.class);
|
||||
private Map<String, ChannelHandler> channelHandlers = new HashMap<String, ChannelHandler>();
|
||||
private @Nullable ScheduledFuture<?> pollingFuture = null;
|
||||
private IpObserverConfiguration config = new IpObserverConfiguration();
|
||||
private String idPass = "";
|
||||
// Config settings parsed from weather station.
|
||||
private boolean imperialTemperature = 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);
|
||||
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
|
||||
@ -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() {
|
||||
if (imperialTemperature) {
|
||||
logger.debug("Using imperial units of measurement for temperature.");
|
||||
@ -332,12 +394,20 @@ public class IpObserverHandler extends BaseThingHandler {
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(IpObserverConfiguration.class);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
|
||||
if (!config.id.isBlank() && !config.password.isBlank()) {
|
||||
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
|
||||
public void dispose() {
|
||||
ipObserverUpdateReceiver.removeStation(this);
|
||||
channelHandlers.clear();
|
||||
ScheduledFuture<?> localFuture = pollingFuture;
|
||||
if (localFuture != null) {
|
||||
|
@ -28,6 +28,7 @@ import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.http.HttpService;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_WEATHER_STATION);
|
||||
private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
|
||||
protected final HttpClient httpClient;
|
||||
|
||||
@Activate
|
||||
public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
|
||||
public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
||||
@Reference HttpService httpService) {
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
ipObserverUpdateReceiver = new IpObserverUpdateReceiver(httpService);
|
||||
}
|
||||
|
||||
protected HttpClient getHttpClient() {
|
||||
@ -60,7 +64,7 @@ public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_WEATHER_STATION.equals(thingTypeUID)) {
|
||||
return new IpObserverHandler(thing, httpClient);
|
||||
return new IpObserverHandler(thing, httpClient, ipObserverUpdateReceiver);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.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.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.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.solarRadiation.label = 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.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.description = UV
|
||||
channel-type.ipobserver.uvIndex.label = UV Index
|
||||
|
@ -10,6 +10,8 @@
|
||||
<channels>
|
||||
<channel id="temperatureIndoor" typeId="temperatureIndoor"/>
|
||||
<channel id="temperatureOutdoor" typeId="system.outdoor-temperature"/>
|
||||
<channel id="temperatureWindChill" typeId="temperatureWindChill"/>
|
||||
<channel id="temperatureDewPoint" typeId="temperatureDewPoint"/>
|
||||
<channel id="humidityIndoor" typeId="humidityIndoor"/>
|
||||
<channel id="humidityOutdoor" typeId="system.atmospheric-humidity"/>
|
||||
<channel id="pressureAbsolute" typeId="pressureAbsolute"/>
|
||||
@ -51,6 +53,16 @@
|
||||
feature allowing you to manually trigger or use a rule to handle the reboots</description>
|
||||
<default>2000</default>
|
||||
</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>
|
||||
</thing-type>
|
||||
<channel-type id="responseTime" advanced="true">
|
||||
@ -70,6 +82,20 @@
|
||||
</tags>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
</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">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Indoor Humidity</label>
|
||||
@ -174,10 +200,6 @@
|
||||
<label>Wind Max Gust</label>
|
||||
<description>Max wind gust for today</description>
|
||||
<category>Wind</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Wind</tag>
|
||||
</tags>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="rainHourlyRate">
|
||||
@ -207,10 +229,6 @@
|
||||
<label>Rain for Week</label>
|
||||
<description>Weekly Rain</description>
|
||||
<category>Rain</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Rain</tag>
|
||||
</tags>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="rainForMonth" advanced="true">
|
||||
@ -218,10 +236,6 @@
|
||||
<label>Rain for Month</label>
|
||||
<description>Rain since 12:00 on the 1st of this month</description>
|
||||
<category>Rain</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Rain</tag>
|
||||
</tags>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="rainForYear">
|
||||
@ -229,10 +243,6 @@
|
||||
<label>Rain for Year</label>
|
||||
<description>Total rain since 12:00 on 1st Jan</description>
|
||||
<category>Rain</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Rain</tag>
|
||||
</tags>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="lastUpdatedTime" advanced="true">
|
||||
|
Loading…
Reference in New Issue
Block a user