mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-03-04 05:16:45 +00:00
582 lines
19 KiB
Perl
Executable File
582 lines
19 KiB
Perl
Executable File
# $Id$
|
|
##############################################################################
|
|
#
|
|
# 98_GEOFANCY.pm
|
|
# An FHEM Perl module to receive geofencing webhooks from geofancy.com.
|
|
#
|
|
# Copyright by Julian Pawlowski
|
|
# e-mail: julian.pawlowski at gmail.com
|
|
#
|
|
# Based on HTTPSRV from Dr. Boris Neubert
|
|
#
|
|
# 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/>.
|
|
#
|
|
#
|
|
# Version: 1.1.2
|
|
#
|
|
# Major Version History:
|
|
# - 1.1.0 - 2014-02-06
|
|
# -- Support for both apps: Geofency and Geofancy
|
|
#
|
|
# - 1.0.0 - 2014-01-09
|
|
# -- First release
|
|
#
|
|
##############################################################################
|
|
|
|
package main;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use vars qw(%data);
|
|
use HttpUtils;
|
|
use Time::Local;
|
|
use Data::Dumper;
|
|
|
|
no if $] >= 5.017011, warnings => 'experimental::smartmatch';
|
|
|
|
sub GEOFANCY_Set($@);
|
|
sub GEOFANCY_Define($$);
|
|
sub GEOFANCY_Undefine($$);
|
|
|
|
#########################
|
|
sub GEOFANCY_addExtension($$$) {
|
|
my ( $name, $func, $link ) = @_;
|
|
|
|
my $url = "/$link";
|
|
Log3 $name, 2, "Registering GEOFANCY $name for URL $url...";
|
|
$data{FWEXT}{$url}{deviceName} = $name;
|
|
$data{FWEXT}{$url}{FUNC} = $func;
|
|
$data{FWEXT}{$url}{LINK} = $link;
|
|
}
|
|
|
|
#########################
|
|
sub GEOFANCY_removeExtension($) {
|
|
my ($link) = @_;
|
|
|
|
my $url = "/$link";
|
|
my $name = $data{FWEXT}{$url}{deviceName};
|
|
Log3 $name, 2, "Unregistering GEOFANCY $name for URL $url...";
|
|
delete $data{FWEXT}{$url};
|
|
}
|
|
|
|
###################################
|
|
sub GEOFANCY_Initialize($) {
|
|
my ($hash) = @_;
|
|
|
|
Log3 $hash, 5, "GEOFANCY_Initialize: Entering";
|
|
|
|
$hash->{SetFn} = "GEOFANCY_Set";
|
|
$hash->{DefFn} = "GEOFANCY_Define";
|
|
$hash->{UndefFn} = "GEOFANCY_Undefine";
|
|
$hash->{AttrList} = "devAlias " . $readingFnAttributes;
|
|
}
|
|
|
|
###################################
|
|
sub GEOFANCY_Define($$) {
|
|
|
|
my ( $hash, $def ) = @_;
|
|
|
|
my @a = split( "[ \t]+", $def, 5 );
|
|
|
|
return "Usage: define <name> GEOFANCY <infix>"
|
|
if ( int(@a) != 3 );
|
|
my $name = $a[0];
|
|
my $infix = $a[2];
|
|
|
|
$hash->{fhem}{infix} = $infix;
|
|
|
|
GEOFANCY_addExtension( $name, "GEOFANCY_CGI", $infix );
|
|
|
|
readingsBeginUpdate($hash);
|
|
readingsBulkUpdate( $hash, "state", "initialized" );
|
|
readingsEndUpdate( $hash, 1 );
|
|
return undef;
|
|
}
|
|
|
|
###################################
|
|
sub GEOFANCY_Undefine($$) {
|
|
|
|
my ( $hash, $name ) = @_;
|
|
|
|
GEOFANCY_removeExtension( $hash->{fhem}{infix} );
|
|
|
|
return undef;
|
|
}
|
|
|
|
###################################
|
|
sub GEOFANCY_Set($@) {
|
|
my ( $hash, @a ) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $state = $hash->{STATE};
|
|
|
|
Log3 $name, 5, "GEOFANCY $name: called function GEOFANCY_Set()";
|
|
|
|
return "No Argument given" if ( !defined( $a[1] ) );
|
|
|
|
my $usage = "Unknown argument " . $a[1] . ", choose one of clear:readings";
|
|
|
|
# clear
|
|
if ( $a[1] eq "clear" ) {
|
|
Log3 $name, 2, "GEOFANCY set $name " . $a[1];
|
|
|
|
if ( $a[2] ) {
|
|
|
|
# readings
|
|
if ( $a[2] eq "readings" ) {
|
|
delete $hash->{READINGS};
|
|
readingsBeginUpdate($hash);
|
|
readingsBulkUpdate( $hash, "state", "clearedReadings" );
|
|
readingsEndUpdate( $hash, 1 );
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
return "No Argument given, choose one of readings ";
|
|
}
|
|
}
|
|
|
|
# return usage hint
|
|
else {
|
|
return $usage;
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
############################################################################################################
|
|
#
|
|
# Begin of helper functions
|
|
#
|
|
############################################################################################################
|
|
|
|
###################################
|
|
sub GEOFANCY_CGI() {
|
|
|
|
# Geofancy
|
|
# /$infix?device=UUIDdev&id=UUIDloc&latitude=xx.x&longitude=xx.x&trigger=(enter|exit)
|
|
#
|
|
# Geofency
|
|
# /$infix?id=UUIDloc&name=locName&entry=(1|0)&date=DATE&latitude=xx.x&longitude=xx.x&device=UUIDdev
|
|
my ($request) = @_;
|
|
|
|
my $hash;
|
|
my $name = "";
|
|
my $link = "";
|
|
my $URI = "";
|
|
my $device = "";
|
|
my $id = "";
|
|
my $lat = "";
|
|
my $long = "";
|
|
my $address = "-";
|
|
my $entry = "";
|
|
my $msg = "";
|
|
my $date = "";
|
|
my $time = "";
|
|
my $locName = "";
|
|
|
|
# data received
|
|
if ( $request =~ m,^(/[^/]+?)(?:\&|\?)(.*)?$, ) {
|
|
$link = $1;
|
|
$URI = $2;
|
|
|
|
# get device name
|
|
$name = $data{FWEXT}{$link}{deviceName} if ( $data{FWEXT}{$link} );
|
|
|
|
# return error if no such device
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK No GEOFANCY device for webhook $link" )
|
|
unless ($name);
|
|
|
|
# extract values from URI
|
|
my $webArgs;
|
|
foreach my $pv ( split( "&", $URI ) ) {
|
|
next if ( $pv eq "" );
|
|
$pv =~ s/\+/ /g;
|
|
$pv =~ s/%([\dA-F][\dA-F])/chr(hex($1))/ige;
|
|
my ( $p, $v ) = split( "=", $pv, 2 );
|
|
|
|
$webArgs->{$p} = $v;
|
|
}
|
|
|
|
# validate id
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Expected value for 'id' cannot be empty" )
|
|
if ( !defined( $webArgs->{id} ) || $webArgs->{id} eq "" );
|
|
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK No whitespace allowed in id '" . $webArgs->{id} . "'" )
|
|
if ( defined( $webArgs->{id} ) && $webArgs->{id} =~ m/(?:\s)/ );
|
|
|
|
# validate locName
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK No whitespace allowed in id '" . $webArgs->{locName} . "'" )
|
|
if ( defined( $webArgs->{locName} )
|
|
&& $webArgs->{locName} =~ m/(?:\s)/ );
|
|
|
|
# require entry or trigger
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Neither 'entry' nor 'trigger' was specified" )
|
|
if ( !defined( $webArgs->{entry} )
|
|
&& !defined( $webArgs->{trigger} ) );
|
|
|
|
# validate entry
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Expected value for 'entry' cannot be empty" )
|
|
if ( defined( $webArgs->{entry} ) && $webArgs->{entry} eq "" );
|
|
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Value for 'entry' can only be: 1 0" )
|
|
if ( defined( $webArgs->{entry} )
|
|
&& $webArgs->{entry} ne 0
|
|
&& $webArgs->{entry} ne 1 );
|
|
|
|
# validate trigger
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Expected value for 'trigger' cannot be empty" )
|
|
if ( defined( $webArgs->{trigger} ) && $webArgs->{trigger} eq "" );
|
|
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Value for 'trigger' can only be: enter|test exit" )
|
|
if ( defined( $webArgs->{trigger} )
|
|
&& $webArgs->{trigger} ne "enter"
|
|
&& $webArgs->{trigger} ne "test"
|
|
&& $webArgs->{trigger} ne "exit" );
|
|
|
|
# validate date
|
|
return (
|
|
"text/plain; charset=utf-8",
|
|
"NOK Specified date '"
|
|
. $webArgs->{date} . "'"
|
|
. " does not match ISO8601 UTC format (1970-01-01T00:00:00Z)"
|
|
)
|
|
if ( defined( $webArgs->{date} )
|
|
&& $webArgs->{date} !~
|
|
m/(19|20)\d\d-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])Z/
|
|
);
|
|
|
|
# validate locName
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK No whitespace allowed in id '" . $webArgs->{locName} . "'" )
|
|
if ( defined( $webArgs->{locName} )
|
|
&& $webArgs->{locName} =~ m/(?:\s)/ );
|
|
|
|
# validate LAT
|
|
return (
|
|
"text/plain; charset=utf-8",
|
|
"NOK Specified latitude '"
|
|
. $webArgs->{latitude}
|
|
. "' has unexpected format"
|
|
)
|
|
if (
|
|
defined $webArgs->{latitude}
|
|
&& ( $webArgs->{latitude} !~ m/^[0-9]+([.][0-9]+)?$/
|
|
|| $webArgs->{latitude} < -90
|
|
|| $webArgs->{latitude} > 90 )
|
|
);
|
|
|
|
# validate LONG
|
|
return (
|
|
"text/plain; charset=utf-8",
|
|
"NOK Specified longitude '"
|
|
. $webArgs->{longitude}
|
|
. "' has unexpected format"
|
|
)
|
|
if (
|
|
defined $webArgs->{longitude}
|
|
&& ( $webArgs->{longitude} !~ m/^[0-9]+([.][0-9]+)?$/
|
|
|| $webArgs->{longitude} < -180
|
|
|| $webArgs->{longitude} > 180 )
|
|
);
|
|
|
|
# validate device
|
|
return ( "text/plain; charset=utf-8",
|
|
"NOK Expected value for 'device' cannot be empty" )
|
|
if ( !defined( $webArgs->{device} ) || $webArgs->{device} eq "" );
|
|
|
|
return (
|
|
"text/plain; charset=utf-8",
|
|
"NOK No whitespace allowed in device '" . $webArgs->{device} . "'"
|
|
)
|
|
if ( defined( $webArgs->{device} )
|
|
&& $webArgs->{device} =~ m/(?:\s)/ );
|
|
|
|
# Geofancy.app
|
|
if ( defined $webArgs->{trigger} ) {
|
|
$id = $webArgs->{id};
|
|
$entry = $webArgs->{trigger};
|
|
$lat = $webArgs->{latitude};
|
|
$long = $webArgs->{longitude};
|
|
$device = $webArgs->{device};
|
|
}
|
|
|
|
# Geofency.app
|
|
elsif ( defined $webArgs->{entry} ) {
|
|
$id = $webArgs->{id};
|
|
$locName = $webArgs->{name};
|
|
$entry = $webArgs->{entry};
|
|
$date = $webArgs->{date};
|
|
$lat = $webArgs->{latitude};
|
|
$long = $webArgs->{longitude};
|
|
$address = $webArgs->{address} if (defined($webArgs->{address}));
|
|
$device = $webArgs->{device};
|
|
}
|
|
else {
|
|
return "fatal error";
|
|
}
|
|
}
|
|
|
|
# no data received
|
|
else {
|
|
Log3 undef, 3, "GEOFANCY: No data received";
|
|
|
|
return ( "text/plain; charset=utf-8", "NOK No data received" );
|
|
}
|
|
|
|
# return error if unknown trigger
|
|
return ( "text/plain; charset=utf-8", "$entry NOK" )
|
|
if ( $entry ne "enter"
|
|
&& $entry ne "1"
|
|
&& $entry ne "exit"
|
|
&& $entry ne "0"
|
|
&& $entry ne "test" );
|
|
|
|
$hash = $defs{$name};
|
|
|
|
# Device alias handling
|
|
#
|
|
delete $hash->{helper}{device_aliases}
|
|
if $hash->{helper}{device_aliases};
|
|
delete $hash->{helper}{device_names}
|
|
if $hash->{helper}{device_names};
|
|
|
|
if ( defined( $attr{$name}{devAlias} ) ) {
|
|
my @devices = split( ' ', $attr{$name}{devAlias} );
|
|
|
|
if (@devices) {
|
|
foreach (@devices) {
|
|
my @device = split( ':', $_ );
|
|
$hash->{helper}{device_aliases}{ $device[0] } =
|
|
$device[1];
|
|
$hash->{helper}{device_names}{ $device[1] } =
|
|
$device[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
$device = $hash->{helper}{device_aliases}{$device}
|
|
if $hash->{helper}{device_aliases}{$device};
|
|
|
|
Log3 $name, 4,
|
|
"GEOFANCY $name: id=$id name=$locName entry=$entry date=$date lat=$lat long=$long dev=$device";
|
|
|
|
readingsBeginUpdate($hash);
|
|
|
|
# validate date
|
|
if ( $date ne "" ) {
|
|
$hash->{".updateTime"} = GEOFANCY_ISO8601UTCtoLocal($date);
|
|
$hash->{".updateTimestamp"} = FmtDateTime( $hash->{".updateTime"} );
|
|
$time = $hash->{".updateTimestamp"};
|
|
}
|
|
|
|
# use local FHEM time
|
|
else {
|
|
$time = TimeNow();
|
|
}
|
|
|
|
# General readings
|
|
readingsBulkUpdate( $hash, "state",
|
|
"id:$id name:$locName trig:$entry date:$date lat:$lat long:$long address:$address dev:$device"
|
|
);
|
|
|
|
$id = $locName if ( defined($locName) && $locName ne "" );
|
|
|
|
readingsBulkUpdate( $hash, "lastDevice", $device );
|
|
readingsBulkUpdate( $hash, "lastArr", $device . " " . $id )
|
|
if ( $entry eq "enter" || $entry eq "1" );
|
|
readingsBulkUpdate( $hash, "lastDep", $device . " " . $id )
|
|
if ( $entry eq "exit" || $entry eq "0" );
|
|
|
|
if ( $entry eq "enter" || $entry eq "1" || $entry eq "test" ) {
|
|
Log3 $name, 4, "GEOFANCY $name: $device arrived at $id";
|
|
readingsBulkUpdate( $hash, $device, "arrived " . $id );
|
|
readingsBulkUpdate( $hash, "currLoc_" . $device, $id );
|
|
readingsBulkUpdate( $hash, "currLocLat_" . $device, $lat );
|
|
readingsBulkUpdate( $hash, "currLocLong_" . $device, $long );
|
|
readingsBulkUpdate( $hash, "currLocAddr_" . $device, $address );
|
|
readingsBulkUpdate( $hash, "currLocTime_" . $device, $time );
|
|
}
|
|
elsif ( $entry eq "exit" || $entry eq "0" ) {
|
|
my $currReading;
|
|
my $lastReading;
|
|
|
|
Log3 $name, 4, "GEOFANCY $name: $device left $id and is underway";
|
|
|
|
# backup last known location if not "underway"
|
|
$currReading = "currLoc_" . $device;
|
|
if ( defined( $hash->{READINGS}{$currReading}{VAL} )
|
|
&& $hash->{READINGS}{$currReading}{VAL} ne "underway" )
|
|
{
|
|
foreach ( 'Loc', 'LocLat', 'LocLong' ) {
|
|
$currReading = "curr" . $_ . "_" . $device;
|
|
$lastReading = "last" . $_ . "_" . $device;
|
|
readingsBulkUpdate( $hash, $lastReading,
|
|
$hash->{READINGS}{$currReading}{VAL} )
|
|
if ( defined( $hash->{READINGS}{$currReading}{VAL} ) );
|
|
}
|
|
$currReading = "currLocTime_" . $device;
|
|
readingsBulkUpdate(
|
|
$hash,
|
|
"lastLocArr_" . $device,
|
|
$hash->{READINGS}{$currReading}{VAL}
|
|
) if ( defined( $hash->{READINGS}{$currReading}{VAL} ) );
|
|
readingsBulkUpdate( $hash, "lastLocDep_" . $device, $time );
|
|
}
|
|
|
|
readingsBulkUpdate( $hash, $device, "left " . $id );
|
|
readingsBulkUpdate( $hash, "currLoc_" . $device, "underway" );
|
|
readingsBulkUpdate( $hash, "currLocLat_" . $device, "-" );
|
|
readingsBulkUpdate( $hash, "currLocLong_" . $device, "-" );
|
|
readingsBulkUpdate( $hash, "currLocAddr_" . $device, "-" );
|
|
readingsBulkUpdate( $hash, "currLocTime_" . $device, $time );
|
|
}
|
|
|
|
readingsEndUpdate( $hash, 1 );
|
|
|
|
$msg = "$entry OK";
|
|
$msg .= "\ndevice=$device id=$id lat=$lat long=$long trig=$entry"
|
|
if ( $entry eq "test" );
|
|
|
|
return ( "text/plain; charset=utf-8", $msg );
|
|
}
|
|
|
|
sub GEOFANCY_ISO8601UTCtoLocal ($) {
|
|
my ($datetime) = @_;
|
|
$datetime =~ s/T/ /g if ( defined( $datetime && $datetime ne "" ) );
|
|
$datetime =~ s/Z//g if ( defined( $datetime && $datetime ne "" ) );
|
|
|
|
my (
|
|
$date, $time, $y, $m, $d, $hour,
|
|
$min, $sec, $hours, $minutes, $seconds, $timestamp
|
|
);
|
|
|
|
( $date, $time ) = split( ' ', $datetime );
|
|
( $y, $m, $d ) = split( '-', $date );
|
|
( $hour, $min, $sec ) = split( ':', $time );
|
|
$m -= 01;
|
|
$timestamp = timegm( $sec, $min, $hour, $d, $m, $y );
|
|
( $sec, $min, $hour, $d, $m, $y ) = localtime($timestamp);
|
|
$timestamp = timelocal( $sec, $min, $hour, $d, $m, $y );
|
|
|
|
return $timestamp;
|
|
}
|
|
|
|
1;
|
|
|
|
=pod
|
|
|
|
=begin html
|
|
|
|
<p>
|
|
<a name="GEOFANCY" id="GEOFANCY"></a>
|
|
</p>
|
|
<h3>
|
|
GEOFANCY
|
|
</h3>
|
|
<ul>
|
|
<li>Provides webhook receiver for geofencing, e.g. via the following apps:<br>
|
|
<br>
|
|
</li>
|
|
<li>
|
|
<a href="https://itunes.apple.com/de/app/geofency-time-tracking-automatic/id615538630?l=en&mt=8">Geofency (iOS - the original app)</a>
|
|
</li>
|
|
<li>
|
|
<a href="https://itunes.apple.com/de/app/geofancy/id725198453?l=en&mt=8">Geofancy (iOS)</a>
|
|
</li>
|
|
<li>
|
|
<a href="http://www.egigeozone.de">EgiGeoZone (Android)</a>
|
|
</li>
|
|
<li>
|
|
<p>
|
|
Note: GEOFANCY is an extension to <a href="FHEMWEB">FHEMWEB</a>. You need to install FHEMWEB to use GEOFANCY.
|
|
</p><a name="GEOFANCYdefine" id="GEOFANCYdefine"></a> <b>Define</b>
|
|
<div style="margin-left: 2em">
|
|
<code>define <name> GEOFANCY <infix></code><br>
|
|
<br>
|
|
Defines the webhook server. <code><infix></code> is the portion behind the FHEMWEB base URL (usually <code>http://hostname:8083/fhem</code>)<br>
|
|
<br>
|
|
Example:
|
|
<div style="margin-left: 2em">
|
|
<code>define geofancy GEOFANCY geo</code><br>
|
|
</div><br>
|
|
The webhook will be reachable at http://hostname:8083/fhem/geo in that case.<br>
|
|
<br>
|
|
</div><a name="GEOFANCYset" id="GEOFANCYset"></a> <b>Set</b>
|
|
<ul>
|
|
<li>
|
|
<b>clear</b> readings can be used to cleanup auto-created readings from deprecated devices.
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<a name="GEOFANCYattr" id="GEOFANCYattr"></a> <b>Attributes</b><br>
|
|
<br>
|
|
<ul>
|
|
<li>devAlias: can be used to rename device names in the format DEVICEUUID:Aliasname. Separate using blank to rename multiple devices.
|
|
</li>
|
|
</ul><br>
|
|
<br>
|
|
<b>Usage information</b><br>
|
|
<br>
|
|
<div style="margin-left: 2em">
|
|
Likely your FHEM installation is not reachable directly from the internet (good idea!).<br>
|
|
It is recommended to have a reverse proxy like nginx or Apache in front of FHEM where you can make sure access is only possible to specific subdirectories like /fhem/geo.<br>
|
|
You might also want to think about protecting the access by using HTTP Basic Authentication and encryption via SSL.<br>
|
|
Also the definition of a dedicated FHEMWEB instance for that purpose might help to restrict FHEM's functionality (note that the 'hidden' attributes of FHEMWEB currently do NOT protect from just guessing/knowing the correct URL!)<br>
|
|
<br>
|
|
To make that reverse proxy available from the internet, just forward the appropriate port via your internet router.<br>
|
|
<br>
|
|
The actual solution on how you can securely make your Geofancy webhook available to the internet is not part of this documentation and depends on your own skills.
|
|
</div><br>
|
|
<br>
|
|
<b>Integration with Home Automation</b><br>
|
|
<br>
|
|
<div style="margin-left: 2em">
|
|
You might want to have a look to the module family of <a href="#ROOMMATE">ROOMMATE</a>, <a href="#GUEST">GUEST</a> and <a href="#RESIDENTS">RESIDENTS</a> for an easy processing of GEOFANCY events.
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=begin html_DE
|
|
|
|
<p>
|
|
<a name="GEOFANCY" id="GEOFANCY"></a>
|
|
</p>
|
|
<h3>
|
|
GEOFANCY
|
|
</h3>
|
|
<div style="margin-left: 2em">
|
|
Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden:
|
|
</div>
|
|
<div style="margin-left: 2em">
|
|
<a href='http://fhem.de/commandref.html#GEOFANCY'>GEOFANCY</a>
|
|
</div>
|
|
|
|
=end html_DE
|
|
|
|
=cut
|