2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-03-09 20:57:11 +00:00
fhem-mirror/fhem/FHEM/59_Weather.pm
borisneubert bf3d1a0700 introduced readingsBulkUpdate, readingsSingleUpdate
introduced GPLv2

git-svn-id: https://svn.fhem.de/fhem/trunk@2099 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2012-11-08 20:56:21 +00:00

606 lines
21 KiB
Perl
Executable File

# $Id$
##############################################################################
#
# 59_Weather.pm
# Copyright by Dr. Boris Neubert
# e-mail: omega at online dot de
#
# This file is part of fhem.
#
# Fhem is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Fhem is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
package main;
use strict;
use warnings;
use Time::HiRes qw(gettimeofday);
#
# uses the Yahoo! Weather API: http://developer.yahoo.com/weather/
#
# Mapping / translation of current weather codes 0-47
my @YahooCodes_en = (
'tornado', 'tropical storm', 'hurricane', 'severe thunderstorms', 'thunderstorms', 'mixed rain and snow',
'mixed rain and sleet', 'mixed snow and sleet', 'freezing drizzle', 'drizzle', 'freezing rain' ,'showers',
'showers', 'snow flurries', 'light snow showers', 'blowing snow', 'snow', 'hail',
'sleet', 'dust', 'foggy', 'haze', 'smoky', 'blustery',
'windy', 'cold', 'cloudy',
'mostly cloudy', # night
'mostly cloudy', # day
'partly cloudy', # night
'partly cloudy', # day
'clear', #night
'sunny',
'fair', #night
'fair', #day
'mixed rain and hail',
'hot', 'isolated thunderstorms', 'scattered thunderstorms', 'scattered thunderstorms', 'scattered showers', 'heavy snow',
'scattered snow showers', 'heavy snow', 'partly cloudy', 'thundershowers', 'snow showers', 'isolated thundershowers');
my @YahooCodes_de = (
'Tornado', 'schwerer Sturm', 'Sturm', 'schwere Gewitter', 'Gewitter', 'Regen und Schnee',
'Regen und Graupel', 'Schnee und Graupel', 'Eisregen', 'Nieselregen', 'gefrierender Regen' ,'Schauer',
'Schauer', 'Schneetreiben', 'leichter Schneeschauer', 'Schneeverwehungen', 'Schnee', 'Hagel',
'Graupel', 'Staub', 'Nebel', 'Dunst', 'Smog', 'Sturm',
'windig', 'kalt', 'wolkig',
'überwiegend wolkig', # night
'überwiegend wolkig', # day
'teilweise wolkig', # night
'teilweise wolkig', # day
'klar', # night
'sonnig',
'bewölkt', # night
'bewölkt', # day
'Regen und Hagel',
'heiß', 'einzelne Gewitter', 'vereinzelt Gewitter', 'vereinzelt Gewitter', 'vereinzelt Schauer', 'starker Schneefall',
'vereinzelt Schneeschauer', 'starker Schneefall', 'teilweise wolkig', 'Gewitterregen', 'Schneeschauer', 'vereinzelt Gewitter');
my @YahooCodes_nl = (
'tornado', 'zware storm', 'orkaan', 'hevig onweer', 'onweer',
'regen en sneeuw',
'regen en ijzel', 'sneeuw en ijzel', 'aanvriezende motregen',
'motregen', 'aanvriezende regen' ,'regenbuien',
'buien', 'sneeuw windstoten', 'lichte sneeuwbuien',
'stuifsneeuw', 'sneeuw', 'hagel',
'ijzel', 'stof', 'mist', 'waas', 'smog', 'heftig',
'winderig', 'koud', 'bewolkt',
'overwegend bewolkt', # night
'overwegend bewolkt', # day
'gedeeltelijk bewolkt', # night
'gedeeltelijk bewolkt', # day
'helder', #night
'zonnig',
'bewolkt', #night
'bewolkt', #day
'regen en hagel',
'heet', 'plaatselijk onweer', 'af en toe onweer', 'af en toe onweer', 'af en toe regenbuien', 'hevige sneeuwval',
'af en toe sneeuwbuien', 'hevige sneeuwval', 'deels bewolkt',
'onweersbuien', 'sneeuwbuien', 'af en toe onweersbuien');
my %pressure_trend_txt_en = ( 0 => "steady", 1 => "rising", 2 => "falling" );
my %pressure_trend_txt_de = ( 0 => "gleichbleibend", 1 => "steigend", 2 => "fallend" );
my %pressure_trend_txt_nl = ( 0 => "stabiel", 1 => "stijgend", 2 => "dalend" );
my %pressure_trend_sym = ( 0 => "=", 1 => "+", 2 => "-" );
my @directions_txt_en = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW');
my @directions_txt_de = ('N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW');
my @directions_txt_nl = ('N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW');
my %wdays_txt_en = ('Mon' => 'Mon', 'Tue' => 'Tue', 'Wed'=> 'Wed', 'Thu' => 'Thu', 'Fri' => 'Fri', 'Sat' => 'Sat', 'Sun' => 'Sun');
my %wdays_txt_de = ('Mon' => 'Mo', 'Tue' => 'Di', 'Wed'=> 'Mi', 'Thu' => 'Do', 'Fri' => 'Fr', 'Sat' => 'Sa', 'Sun' => 'So');
my %wdays_txt_nl = ('Mon' => 'Maa', 'Tue' => 'Din', 'Wed'=> 'Woe', 'Thu' => 'Don', 'Fri' => 'Vri', 'Sat' => 'Zat', 'Sun' => 'Zon');
my @iconlist = (
'storm', 'storm', 'storm', 'thunderstorm', 'thunderstorm', 'rainsnow',
'sleet', 'snow', 'drizzle', 'drizzle', 'icy' ,'chance_of_rain',
'chance_of_rain', 'snowflurries', 'chance_of_snow', 'heavysnow', 'snow', 'heavyrain',
'sleet', 'dust', 'fog', 'haze', 'smoke', 'flurries',
'windy', 'icy', 'cloudy', 'mostlycloudy_night', 'mostlycloudy', 'partly_cloudy_night',
'partly_cloudy', 'clear', 'sunny', 'mostly_clear_night', 'overcast', 'heavyrain',
'clear', 'scatteredthunderstorms', 'scatteredthunderstorms', 'scatteredthunderstorms', 'scatteredshowers', 'heavysnow',
'chance_of_snow', 'heavysnow', 'partly_cloudy', 'heavyrain', 'chance_of_snow', 'scatteredshowers');
#####################################
sub Weather_Initialize($) {
my ($hash) = @_;
$hash->{DefFn} = "Weather_Define";
$hash->{UndefFn} = "Weather_Undef";
$hash->{GetFn} = "Weather_Get";
$hash->{SetFn} = "Weather_Set";
$hash->{AttrList}= "loglevel:0,1,2,3,4,5 localicons event-on-update-reading event-on-change-reading";
}
###################################
sub latin1_to_utf8($) {
# http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS
my ($s)= @_;
$s =~ s/([\x80-\xFF])/chr(0xC0|ord($1)>>6).chr(0x80|ord($1)&0x3F)/eg;
return $s;
}
###################################
sub temperature_in_c($$) {
my ($temperature, $unitsystem)= @_;
return $unitsystem ne "SI" ? int(($temperature-32)*5/9+0.5) : $temperature;
}
sub wind_in_km_per_h($$) {
my ($wind, $unitsystem)= @_;
return $unitsystem ne "SI" ? int(1.609344*$wind+0.5) : $wind;
}
sub degrees_to_direction($@) {
my ($degrees,@directions_txt_i18n) = @_;
my $mod = int((($degrees + 11.25) % 360) / 22.5);
return $directions_txt_i18n[$mod];
}
###################################
sub Weather_UpdateReading($$$$) {
my ($hash,$prefix,$key,$value)= @_;
#Log 1, "DEBUG WEATHER: $prefix $key $value";
my $unitsystem= $hash->{READINGS}{unit_system}{VAL};
if($key eq "low") {
$key= "low_c";
$value= temperature_in_c($value,$unitsystem);
} elsif($key eq "high") {
$key= "high_c";
$value= temperature_in_c($value,$unitsystem);
} elsif($key eq "humidity") {
# standardize reading - allow generic logging of humidity.
$value=~ s/.*?(\d+).*/$1/; # extract numeric
}
my $reading= $prefix . $key;
readingsBulkUpdate($hash,$reading,$value);
if($reading eq "temp_c") {
readingsBulkUpdate($hash,"temperature",$value); # additional entry for compatibility
}
if($reading eq "wind_condition") {
$value=~ s/.*?(\d+).*/$1/; # extract numeric
readingsBulkUpdate($hash,"wind",wind_in_km_per_h($value,$unitsystem)); # additional entry for compatibility
}
return 1;
}
###################################
sub Weather_RetrieveData($)
{
my ($hash)= @_;
my $location= $hash->{LOCATION}; # WOEID [WHERE-ON-EARTH-ID], go to http://weather.yahoo.com to find out
my $units= $hash->{UNITS};
my $fc = undef;
my $xml = GetFileFromURL("http://weather.yahooapis.com/forecastrss?w=" . $location . "&u=" . $units, 3, undef, 1);
return 0 if( ! defined $xml || $xml eq "");
my $lang= $hash->{LANG};
my @YahooCodes_i18n;
my %wdays_txt_i18n;
my @directions_txt_i18n;
my %pressure_trend_txt_i18n;
if($lang eq "de") {
@YahooCodes_i18n= @YahooCodes_de;
%wdays_txt_i18n= %wdays_txt_de;
@directions_txt_i18n= @directions_txt_de;
%pressure_trend_txt_i18n= %pressure_trend_txt_de;
} elsif($lang eq "nl") {
@YahooCodes_i18n= @YahooCodes_nl;
%wdays_txt_i18n= %wdays_txt_nl;
@directions_txt_i18n= @directions_txt_nl;
%pressure_trend_txt_i18n= %pressure_trend_txt_nl;
} else {
@YahooCodes_i18n= @YahooCodes_en;
%wdays_txt_i18n= %wdays_txt_en;
@directions_txt_i18n= @directions_txt_en;
%pressure_trend_txt_i18n= %pressure_trend_txt_en;
}
foreach my $l (split("<",$xml)) {
#Log 1, "DEBUG WEATHER: line=\"$l\"";
next if($l eq ""); # skip empty lines
$l =~ s/(\/|\?)?>$//; # strip off /> and >
my ($tag,$value)= split(" ", $l, 2); # split tag data=..... at the first blank
next if(!defined($tag) || ($tag !~ /^yweather:/));
$fc= 0 if($tag eq "yweather:condition");
$fc++ if($tag eq "yweather:forecast");
my $prefix= $fc ? "fc" . $fc ."_" : "";
### location
if ($tag eq "yweather:location" ) {
$value =~/city="(.*?)" .*country="(.*?)".*/;
my $loc = "";
$loc = $1 if (defined($1));
$loc .= ", $2" if (defined($2));
readingsBulkUpdate($hash, "city", $loc);
}
### current condition and forecast
if (($tag eq "yweather:condition" ) || ($tag eq "yweather:forecast" )) {
my $code = (($value =~/code="([0-9]*?)".*/) ? $1 : undef);
if (defined($code)) {
readingsBulkUpdate($hash, $prefix . "code", $code);
my $text = $YahooCodes_i18n[$code];
if ($text) { readingsBulkUpdate($hash, $prefix . "condition", $text); }
#### add icon logic here - generate from code
$text = $iconlist[$code];
readingsBulkUpdate($hash, $prefix . "icon", $text) if ($text);
}
}
### current condition
if ($tag eq "yweather:condition" ) {
my $temp = (($value =~/temp="(-?[0-9.]*?)".*/) ? $1 : undef);
if ($temp) {
readingsBulkUpdate($hash, "temperature", $temp);
readingsBulkUpdate($hash, "temp_c", $temp); # compatibility
$temp = int(( $temp * 9 / 5 ) + 32.5); # Celsius to Fahrenheit
readingsBulkUpdate($hash, "temp_f", $temp); # compatibility
}
my $datum = (($value =~/date=".*? ([0-9].*)".*/) ? $1 : undef);
readingsBulkUpdate($hash, "current_date_time", $datum) if (defined($1));
my $day = (($value =~/date="(.*?), .*/) ? $1 : undef);
if ($day) {
readingsBulkUpdate($hash, "day_of_week", $wdays_txt_i18n{$day});
}
}
### forecast
if ($tag eq "yweather:forecast" ) {
my $low_c = (($value =~/low="(-?[0-9.]*?)".*/) ? $1 : undef);
if ($low_c) { readingsBulkUpdate($hash, $prefix . "low_c", $low_c); }
my $high_c = (($value =~/high="(-?[0-9.]*?)".*/) ? $1 : undef);
if ($high_c) { readingsBulkUpdate($hash, $prefix . "high_c", $high_c); }
my $day1 = (($value =~/day="(.*?)" .*/) ? $1 : undef); # forecast
if ($day1) {
readingsBulkUpdate($hash, $prefix . "day_of_week", $wdays_txt_i18n{$day1});
}
}
### humidiy / Pressure
if ($tag eq "yweather:atmosphere" ) {
$value =~/humidity="([0-9.]*?)" .*visibility="([0-9.]*?|\s*?)" .*pressure="([0-9.]*?)" .*rising="([0-9.]*?)" .*/;
if ($1) { readingsBulkUpdate($hash, "humidity", $1); }
my $vis = (($2 eq "") ? " " : int($2+0.5)); # clear visibility field
readingsBulkUpdate($hash, "visibility", $vis);
if ($3) { readingsBulkUpdate($hash, "pressure", int($3+0.5)); }
if ($4) {
readingsBulkUpdate($hash, "pressure_trend", $4);
readingsBulkUpdate($hash, "pressure_trend_txt", $pressure_trend_txt_i18n{$4});
readingsBulkUpdate($hash, "pressure_trend_sym", $pressure_trend_sym{$4});
}
}
### wind
if ($tag eq "yweather:wind" ) {
$value =~/chill="(-?[0-9.]*?)" .*direction="([0-9.]*?)" .*speed="([0-9.]*?)" .*/;
readingsBulkUpdate($hash, "wind_chill", $1) if (defined($1));
readingsBulkUpdate($hash, "wind_direction", $2) if (defined($2));
my $windspeed= defined($3) ? int($3+0.5) : "";
readingsBulkUpdate($hash, "wind_speed", $windspeed);
readingsBulkUpdate($hash, "wind", $windspeed); # duplicate for compatibility
if (defined($2) & defined($3)) {
my $wdir = degrees_to_direction($2,@directions_txt_i18n);
readingsBulkUpdate($hash, "wind_condition", "Wind: $wdir $windspeed km/h"); # compatibility
}
}
}
} #end sub
###################################
sub Weather_GetUpdate($)
{
my ($hash) = @_;
if(!$hash->{LOCAL}) {
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Weather_GetUpdate", $hash, 1);
}
readingsBeginUpdate($hash);
Weather_RetrieveData($hash);
my $temperature= $hash->{READINGS}{temperature}{VAL};
my $humidity= $hash->{READINGS}{humidity}{VAL};
my $wind= $hash->{READINGS}{wind}{VAL};
my $val= "T: $temperature H: $humidity W: $wind";
Log GetLogLevel($hash->{NAME},4), "Weather ". $hash->{NAME} . ": $val";
$hash->{STATE}= $val;
addEvent($hash, $val);
readingsEndUpdate($hash, defined($hash->{LOCAL} ? 0 : 1)); # DoTrigger, because sub is called by a timer instead of dispatch
return 1;
}
# Perl Special: { $defs{Weather}{READINGS}{condition}{VAL} }
# conditions: Mostly Cloudy, Overcast, Clear, Chance of Rain
###################################
sub Weather_Get($@) {
my ($hash, @a) = @_;
return "argument is missing" if(int(@a) != 2);
$hash->{LOCAL} = 1;
Weather_GetUpdate($hash);
delete $hash->{LOCAL};
my $reading= $a[1];
my $value;
if(defined($hash->{READINGS}{$reading})) {
$value= $hash->{READINGS}{$reading}{VAL};
} else {
return "no such reading: $reading";
}
return "$a[0] $reading => $value";
}
###################################
sub Weather_Set($@) {
my ($hash, @a) = @_;
my $cmd= $a[1];
# usage check
if((@a == 2) && ($a[1] eq "update")) {
RemoveInternalTimer($hash);
Weather_GetUpdate($hash);
return undef;
} else {
return "Unknown argument $cmd, choose one of update";
}
}
#####################################
sub Weather_Define($$) {
my ($hash, $def) = @_;
# define <name> Weather <location> [interval]
# define MyWeather Weather "Maintal,HE" 3600
my @a = split("[ \t][ \t]*", $def);
return "syntax: define <name> Weather <location> [interval [en|de|nl]]"
if(int(@a) < 3 && int(@a) > 5);
$hash->{STATE} = "Initialized";
$hash->{fhem}{interfaces}= "temperature;humidity;wind";
my $name = $a[0];
my $location = $a[2];
my $interval = 3600;
my $lang = "en";
if(int(@a)>=4) { $interval= $a[3]; }
if(int(@a)==5) { $lang= $a[4]; }
$hash->{LOCATION} = $location;
$hash->{INTERVAL} = $interval;
$hash->{LANG} = $lang;
$hash->{UNITS} = "c"; # hardcoded to use degrees centigrade (Celsius)
$hash->{READINGS}{current_date_time}{TIME}= TimeNow();
$hash->{READINGS}{current_date_time}{VAL}= "none";
$hash->{LOCAL} = 1;
Weather_GetUpdate($hash);
delete $hash->{LOCAL};
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Weather_GetUpdate", $hash, 0);
return undef;
}
#####################################
sub Weather_Undef($$) {
my ($hash, $arg) = @_;
RemoveInternalTimer($hash);
return undef;
}
#####################################
# Icon Parameter
use constant ICONHIGHT => 120;
use constant ICONWIDTH => 175;
use constant ICONSCALE => 0.5;
#####################################
sub
WeatherIconIMGTag($) {
my $width= int(ICONSCALE*ICONWIDTH);
my ($icon)= @_;
my $url= FW_IconURL("weather/$icon");
my $style= " width=$width";
return "<img src=\"$url\"$style alt=\"$icon\">";
}
#####################################
sub
WeatherAsHtml($)
{
my ($d) = @_;
$d = "<none>" if(!$d);
return "$d is not a Weather instance<br>"
if(!$defs{$d} || $defs{$d}{TYPE} ne "Weather");
my $width= int(ICONSCALE*ICONWIDTH);
my $ret = sprintf("<table><tr><th width=%d></th><th></th></tr>", $width);
$ret .= sprintf('<tr><td width=%d>%s</td><td>%s<br>%s°C %s%%<br>%s</td></tr>',
$width,
WeatherIconIMGTag(ReadingsVal($d, "icon", "")),
ReadingsVal($d, "condition", ""),
ReadingsVal($d, "temp_c", ""), ReadingsVal($d, "humidity", ""),
ReadingsVal($d, "wind_condition", ""));
for(my $i=1; $i<=2; $i++) {
# Yahoo provides only 2 days.
#next if (ReadingsVal($d, "fc${i}_code", "") eq ""); # MH skip non existent entries
$ret .= sprintf('<tr><td width=%d>%s</td><td>%s: %s<br>min %s°C max %s°C</td></tr>',
$width,
WeatherIconIMGTag(ReadingsVal($d, "fc${i}_icon", "")),
ReadingsVal($d, "fc${i}_day_of_week", ""),
ReadingsVal($d, "fc${i}_condition", ""),
ReadingsVal($d, "fc${i}_low_c", ""), ReadingsVal($d, "fc${i}_high_c", ""));
}
$ret .= "</table>";
return $ret;
}
#####################################
1;
=pod
=begin html
<a name="Weather"></a>
<h3>Weather</h3>
<ul>
<br>
<a name="Weatherdefine"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; Weather &lt;location&gt; [&lt;interval&gt; [&lt;language&gt;]]</code><br>
<br>
Defines a virtual device for weather forecasts.<br><br>
A Weather device periodically gathers current and forecast weather conditions
from the Yahoo Weather API.<br><br>
The parameter <code>location</code> is the WOEID (WHERE-ON-EARTH-ID), go to
<a href="http://weather.yahoo.com">http://weather.yahoo.com</a> to find it out for your location.<br><br>
The optional parameter <code>interval</code> is the time between subsequent updates
in seconds. It defaults to 3600 (1 hour).<br><br>
The optional language parameter may be one of
<code>de</code>,
<code>en</code>,
<code>nl</code>,
It determines the natural language in which the forecast information appears.
It defaults to <code>en</code>. If you want to set the language you also have to set the interval.<br><br>
Examples:
<pre>
define MyWeather Weather 673513
define Forecast Weather 673513 1800
</pre>
</ul>
<br>
<a name="Weatherset"></a>
<b>Set </b>
<ul>
<code>set &lt;name&gt; update</code><br><br>
Forces the retrieval of the weather data. The next automatic retrieval is scheduled to occur
<code>interval</code> seconds later.<br><br>
</ul>
<br>
<a name="Weatherget"></a>
<b>Get</b>
<ul>
<code>get &lt;name&gt; &lt;reading&gt;</code><br><br>
Valid readings and their meaning (? can be one of 1, 2 and stands
for today, tomorrow):<br>
<table>
<tr><td>city</td><td>name of town returned for location</td></tr>
<tr><td>code</td><td>current condition code</td></tr>
<tr><td>condition</td><td>current condition</td></tr>
<tr><td>current_date_time</td><td>last update of forecast on server</td></tr>
<tr><td>fc?_code</td><td>forecast condition code</td></tr>
<tr><td>fc?_condition</td><td>forecast condition</td></tr>
<tr><td>fc?_day_of_week</td><td>day of week for day +?</td></tr>
<tr><td>fc?_high_c</td><td>forecasted daily high in degrees centigrade</td></tr>
<tr><td>fc?_icon</td><td>forecast icon</td></tr>
<tr><td>fc?_low_c</td><td>forecasted daily low in degrees centigrade</td></tr>
<tr><td>humidity</td><td>current humidity in %</td></tr>
<tr><td>icon</td><td>relative path for current icon</td></tr>
<tr><td>pressure</td><td>air pressure in hPa</td></tr>
<tr><td>pressure_trend</td><td>air pressure trend (0= steady, 1= rising, 2= falling)</td></tr>
<tr><td>pressure_trend_txt</td><td>textual representation of air pressure trend</td></tr>
<tr><td>pressure_trend_sym</td><td>symbolic representation of air pressure trend</td></tr>
<tr><td>temperature</td><td>current temperature in degrees centigrade</td></tr>
<tr><td>temp_c</td><td>current temperature in degrees centigrade</td></tr>
<tr><td>temp_f</td><td>current temperature in degrees Fahrenheit</td></tr>
<tr><td>visibility</td><td>visibility in km</td></tr>
<tr><td>wind</td><td>wind speed in km/h</td></tr>
<tr><td>wind_chill</td><td>wind chill in degrees centigrade</td></tr>
<tr><td>wind_condition</td><td>wind direction and speed</td></tr>
<tr><td>wind_direction</td><td>direction wind comes from in degrees (0 = north wind)</td></tr>
<tr><td>wind_speed</td><td>same as wind</td></tr>
</table>
</ul>
<br>
<a name="Weatherattr"></a>
<b>Attributes</b>
<ul>
<li><a href="#event-on-update-reading">event-on-update-reading</a></li>
<li><a href="#event-on-change-reading">event-on-change-reading</a></li>
</ul>
<br>
</ul>
=end html
=cut