From c76d218f635eeb12f4c0fea7c12b395d1a36a1bc Mon Sep 17 00:00:00 2001 From: MartinEskildsen Date: Mon, 21 May 2018 14:42:12 +0200 Subject: [PATCH] Added image channel, changed to jetty, Optimized performance Signed-off-by: Martin Eskildsen (github: Mr-Eskildsen) --- .../org.openhab.binding.zoneminder/.classpath | 2 +- .../ESH-INF/binding/binding.xml | 14 +- .../ESH-INF/config/monitor-channels.xml | 23 - .../ESH-INF/config/monitor-config.xml | 87 + .../ESH-INF/config/server-config.xml | 137 ++ .../ESH-INF/config/zoneminderserver.xml | 84 - .../ESH-INF/thing/bridge-server.xml | 39 + .../ESH-INF/thing/monitor-thing.xml | 137 -- .../ESH-INF/thing/server-bridge.xml | 39 - .../ESH-INF/thing/thing-monitor.xml | 162 ++ .../META-INF/MANIFEST.MF | 15 +- .../OSGI-INF/ZoneMinderHandlerFactory.xml | 23 +- .../org.openhab.binding.zoneminder/README.md | 45 +- .../build.properties | 1 + .../lib/zoneminder4j-0.9.7.jar | Bin 80119 -> 0 bytes .../lib/zoneminder4j-0.9.8.jar | Bin 0 -> 131554 bytes .../org.openhab.binding.zoneminder/out.txt | 300 +++ .../org.openhab.binding.zoneminder/pom.xml | 1 + .../zoneminder/ZoneMinderConstants.java | 104 +- .../zoneminder/ZoneMinderProperties.java | 4 +- ...erHandler.java => IZoneMinderHandler.java} | 10 +- .../handler/ZoneMinderBaseThingHandler.java | 310 +-- .../ZoneMinderServerBridgeHandler.java | 1711 +++++++++++------ .../ZoneMinderThingMonitorHandler.java | 1265 +++++++----- .../handler/ZoneMinderThingType.java | 2 +- .../internal/DataRefreshPriorityEnum.java | 18 - .../zoneminder/internal/RefreshPriority.java | 89 + .../internal/ZoneMinderConnectionStatus.java | 84 + .../config/ZoneMinderBridgeServerConfig.java | 98 +- .../internal/config/ZoneMinderConfig.java | 12 +- .../config/ZoneMinderThingMonitorConfig.java | 74 +- .../discovery/ZoneMinderDiscoveryService.java | 91 +- .../internal/state/ChannelOnOffType.java | 57 + .../internal/state/ChannelRawType.java | 53 + .../internal/state/ChannelState.java | 37 + .../state/ChannelStateChangePublisher.java | 46 + .../state/ChannelStateChangeSubscriber.java | 26 + .../internal/state/ChannelStringType.java | 48 + .../internal/state/GenericChannelState.java | 111 ++ .../internal/state/GenericThingState.java | 182 ++ .../internal/state/MonitorThingState.java | 537 ++++++ 41 files changed, 4429 insertions(+), 1649 deletions(-) delete mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-channels.xml create mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-config.xml create mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/server-config.xml delete mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/zoneminderserver.xml create mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/bridge-server.xml delete mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/monitor-thing.xml delete mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/server-bridge.xml create mode 100644 addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/thing-monitor.xml delete mode 100644 addons/binding/org.openhab.binding.zoneminder/lib/zoneminder4j-0.9.7.jar create mode 100644 addons/binding/org.openhab.binding.zoneminder/lib/zoneminder4j-0.9.8.jar create mode 100644 addons/binding/org.openhab.binding.zoneminder/out.txt rename addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/{ZoneMinderHandler.java => IZoneMinderHandler.java} (79%) delete mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/DataRefreshPriorityEnum.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/RefreshPriority.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/ZoneMinderConnectionStatus.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelOnOffType.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelRawType.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelState.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangePublisher.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangeSubscriber.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStringType.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericChannelState.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericThingState.java create mode 100644 addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/MonitorThingState.java diff --git a/addons/binding/org.openhab.binding.zoneminder/.classpath b/addons/binding/org.openhab.binding.zoneminder/.classpath index 3801e00168a71..e75fa0c6a7f70 100644 --- a/addons/binding/org.openhab.binding.zoneminder/.classpath +++ b/addons/binding/org.openhab.binding.zoneminder/.classpath @@ -3,6 +3,6 @@ - + diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/binding/binding.xml index d6f42b96fc3ce..f85134801d585 100644 --- a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/binding/binding.xml +++ b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/binding/binding.xml @@ -1,11 +1,9 @@ - - ZoneMinder Binding - This binding interfaces a ZoneMinder Server - Martin S. Eskildsen - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:binding="http://eclipse.org/smarthome/schemas/binding/v1.0.0" + xsi:schemaLocation="http://eclipse.org/smarthome/schemas/binding/v1.0.0 http://eclipse.org/smarthome/schemas/binding-1.0.0.xsd"> + ZoneMinder Binding + This binding interfaces a ZoneMinder Server + Martin S. Eskildsen diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-channels.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-channels.xml deleted file mode 100644 index 0a5f91128a78f..0000000000000 --- a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-channels.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - The ID of the monitor in ZoneMinder - - - - Timeout in seconds when activating alarm. Default is 60 seconds - 60 - - - - Event text in ZoneMinder - Triggered from openHAB - - - diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-config.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-config.xml new file mode 100644 index 0000000000000..89a6b018fb62f --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/monitor-config.xml @@ -0,0 +1,87 @@ + + + + + + advancedConfig + + true + + + + imageConfig + + true + + + + + The ID of the monitor in ZoneMinder + + + + + Timeout in seconds when activating alarm, 0 disables timeout. Default is 60 seconds + 60 + true + + + + + Event text in ZoneMinder + Triggered from openHAB + true + + + + + + Refresh priority for still images when event state is idle + low + + + + + + + + + + Refresh priority for still images when event state is alarmed + normal + + + + + + + + + + + + Rescale image from ZoneMinder Monitor + 100 + + + + + + + + + + + + + + + + + + + + diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/server-config.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/server-config.xml new file mode 100644 index 0000000000000..53736e93a0655 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/server-config.xml @@ -0,0 +1,137 @@ + + + + + basicConfig + + + + credentials + + + + networkAdvConfig + + + + refreshConfig + + + + performanceConfig + + + + streamingConfig + + true + + + + advancedConfig + + + + + + network-address + + The IP address or hostname of the ZoneMinder Server + + + + Protocol to connect to the ZoneMinder Server API (http or https) + http + + + + + + + + User to access the ZoneMinder Server API + + + password + + Password to access the ZoneMinder Server API + + + + + Additional path on ZoneMinder Server to access ZoneMinder Portal page. In a standard installation this is' /zm' + /zm + true + + + + Additional path on ZoneMinder Server to access API. In a standard installation this is' /zm/api' + /zm/api + true + + + + + Port of the ZoneMinder Server API. If '0', then the port will be determined from the protocol + 0 + true + + + + Port to listen for events in (Telnet) + 6802 + true + + + + + Seconds between each call to ZoneMinder Server API to refresh values in openHAB + 10 + true + + + + Seconds between each call to ZoneMinder Server to refresh Server DiskUsage in ZoneMinder. Default value is '60' + 60 + true + + + + + Refresh disk usage counter + disabled + + + + + true + + + + + If enabled new monitors on the ZoneMinder Server will automatically be added to the Inbox in openHAB + true + true + + + + + Specific streaming user + false + true + + + + + Optional User to access image streams + + + + + Optional Password for streaming user + + + + + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/zoneminderserver.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/zoneminderserver.xml deleted file mode 100644 index 2ecfa8cc1c433..0000000000000 --- a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/config/zoneminderserver.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - basic - - - - credentials - - - - network - - - - refreshConfig - - - - advancedSettings - - - - network-address - - The IP address or hostname of the ZoneMinder Server - - - - Protocol to connect to the ZoneMinder Server API (http or https) - http - - - - - - - - Additional path on ZoneMinder Server to access API. In a standard installation this is' /zm' - /zm - - - - User to access the ZoneMinder Server API - - - password - - Password to access the ZoneMinder Server API - - - - Port of the ZoneMinder Server API. If '0', then the port will be determined from the protocol - 0 - true - - - - Port to listen for events in (Telnet) - 6802 - true - - - - Seconds between each call to ZoneMinder Server API to refresh values in openHAB - 10 - true - - - - Minutes between each call to ZoneMinder Server to refresh Server DiskUsage in ZoneMinder. Default value is '0' (Disabled) - 0 - true - - - - If enabled new monitors on the ZoneMinder Server will automatically be added to the Inbox in openHAB - true - true - - - diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/bridge-server.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/bridge-server.xml new file mode 100644 index 0000000000000..56c44a5b9c683 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/bridge-server.xml @@ -0,0 +1,39 @@ + + + + + + ZoneMinder Server + + + + + + + + + + + Switch + + ZoneMinder Server Online Status + + + + + Number + + ZoneMinder Server CPU Load + + + + Number + + ZoneMinder Server Disk Usage + + + + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/monitor-thing.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/monitor-thing.xml deleted file mode 100644 index 1f44e59a69745..0000000000000 --- a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/monitor-thing.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - Camera in ZoneMinder - - - - - - - - - - - - - - - - - - - - - Switch - - Switch telling if the monitor is online - - - - - Switch - - Showing the value of the checkbox 'enabled' in ZoneMinder for the monitor - - - - - Switch - - Will force an alarm from openHAB in ZoneMinder - - - - - - Switch - - set to 'ON' when one of the following is true: Motion detected, Signal lost, Force Alarm pressed, External Alarm. Else set to 'OFF' - - - - - Switch - - set to 'ON' when either channel monitor-alarm set to 'ON', or montior function is 'Mocord' or 'Record'. Else set to 'OFF' - - - - - String - - Current Monitor Status: 0=Idle, 1=Pre-alarm, 2=Alarm, 3=Alert, 4=Recording - - - - - - - - - - - - - String - - Current Monitor Function: None, Monitor, Modect, Record, Mocord, Nodect - - - - - - - - - - - - - - String - - Cause of event: None, Signal, Motion, Forced Web, openHAB, Other - - - - - - - - - - - - - - Switch - - State of ZoneMinder Capture daemon for this monitor - - - - - Switch - - State of ZoneMinder Analysis daemon for this monitor - - - - - Switch - - State of ZoneMinder Frame daemon for this monitor - - - - diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/server-bridge.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/server-bridge.xml deleted file mode 100644 index 4ce2511ea787e..0000000000000 --- a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/server-bridge.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - ZoneMinder Server - - - - - - - - - - - Switch - - ZoneMinder Server Online Status - - - - - Number - - ZoneMinder Server CPU Load - - - - Number - - ZoneMinder Server Disk Usage - - - - diff --git a/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/thing-monitor.xml b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/thing-monitor.xml new file mode 100644 index 0000000000000..2525d81424059 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/ESH-INF/thing/thing-monitor.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + Camera in ZoneMinder + + + + + + + + + + + + + + + + + + + + + + + + Switch + + Switch telling if the monitor is online + + + + + Switch + + Showing the value of the checkbox 'enabled' in ZoneMinder for the monitor + + + + + Switch + + Will force an alarm from openHAB in ZoneMinder + + + + + + Switch + + set to 'ON' when one of the following is true: Motion detected, Signal lost, Force Alarm pressed, External Alarm. Else set to 'OFF' + + + + + Switch + + set to 'ON' when either channel monitor-alarm set to 'ON', or montior function is 'Mocord' or 'Record'. Else set to 'OFF' + + + + + Switch + + set to 'ON' when active alarm caused by motion. Else set to 'OFF' + + + + + String + + Current Monitor Status: 0=Idle, 1=Pre-alarm, 2=Alarm, 3=Alert, 4=Recording + + + + + + + + + + + + + String + + Current Monitor Function: None, Monitor, Modect, Record, Mocord, Nodect + + + + + + + + + + + + + + + String + + Cause of event: None, Signal, Motion, Forced Web, openHAB, Other + + + + + + + + + + + + + + Switch + + State of ZoneMinder Capture daemon for this monitor + + + + + Switch + + State of ZoneMinder Analysis daemon for this monitor + + + + + Switch + + State of ZoneMinder Frame daemon for this monitor + + + + + Image + + Image channel for Monitor + + + + + String + + Video URL for Monitor + + + + diff --git a/addons/binding/org.openhab.binding.zoneminder/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.zoneminder/META-INF/MANIFEST.MF index 6fa47be8643c9..53291ac5d9a75 100644 --- a/addons/binding/org.openhab.binding.zoneminder/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.zoneminder/META-INF/MANIFEST.MF @@ -3,10 +3,10 @@ Bundle-ClassPath: ., lib/gson-2.7.jar, lib/guava-17.0.jar, - lib/jersey-common-2.4.1.jar, lib/javax.ws.rs-api-2.0.1.jar, + lib/jersey-common-2.4.1.jar, lib/jsoup-1.10.1.jar, - lib/zoneminder4j-0.9.7.jar + lib/zoneminder4j-0.9.8.jar Bundle-ManifestVersion: 2 Bundle-Name: ZoneMinder Binding Bundle-RequiredExecutionEnvironment: JavaSE-1.8 @@ -18,13 +18,22 @@ Export-Package: org.openhab.binding.zoneminder.handler Import-Package: com.google.common.collect, + com.google.gson, javax.ws.rs.client, javax.ws.rs.core, org.apache.commons.lang, - org.apache.commons.net.util, org.eclipse.jdt.annotation;resolution:=optional, + org.eclipse.jetty.client, + org.eclipse.jetty.client.api, + org.eclipse.jetty.client.util, + org.eclipse.jetty.http, + org.eclipse.jetty.io, + org.eclipse.jetty.util, + org.eclipse.jetty.util.component, + org.eclipse.jetty.util.ssl, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, + org.eclipse.smarthome.config.discovery.inbox, org.eclipse.smarthome.core.library.types, org.eclipse.smarthome.core.thing, org.eclipse.smarthome.core.thing.binding, diff --git a/addons/binding/org.openhab.binding.zoneminder/OSGI-INF/ZoneMinderHandlerFactory.xml b/addons/binding/org.openhab.binding.zoneminder/OSGI-INF/ZoneMinderHandlerFactory.xml index 73421dcbc315a..a1faa02e54056 100644 --- a/addons/binding/org.openhab.binding.zoneminder/OSGI-INF/ZoneMinderHandlerFactory.xml +++ b/addons/binding/org.openhab.binding.zoneminder/OSGI-INF/ZoneMinderHandlerFactory.xml @@ -1,20 +1,15 @@ - - - - - - - + + + + diff --git a/addons/binding/org.openhab.binding.zoneminder/README.md b/addons/binding/org.openhab.binding.zoneminder/README.md index 8886cb6dbb114..fc9f37d957f92 100644 --- a/addons/binding/org.openhab.binding.zoneminder/README.md +++ b/addons/binding/org.openhab.binding.zoneminder/README.md @@ -3,7 +3,7 @@ This binding offers integration to a ZoneMinder Server. It currently only offers to integrate to monitors (eg. cameras in ZoneMinder). It also only offers access to a limited set of values, as well as a even more limited option to update values in ZoneMinder. It requires at least ZoneMinder 1.29 with API enabled (option 'OPT_USE_API' in ZoneMinder must be enabled). -The option 'OPT_TRIGGERS' must be anabled to allow openHAB to trip the ForceAlarm in ZoneMinder. +The option 'OPT_TRIGGERS' must be enabled to allow openHAB to trip the ForceAlarm in ZoneMinder. ## Supported Things @@ -31,8 +31,8 @@ When a new monitor is discovered it will appear in the Inbox. | Channel | Type | Description | |------------|--------|----------------------------------------------| | online | Switch | Parameter indicating if the server is online | -| CPU load | Text | Current CPU Load of server | -| Disk Usage | text | Current Disk Usage on server | +| cpu-load | Text | Current CPU Load of server | +| disk-usage | Text | Current Disk Usage on server | ### Thing @@ -49,15 +49,48 @@ When a new monitor is discovered it will appear in the Inbox. | capture-daemon | Switch | Run state of ZMC Daemon | | analysis-daemon | Switch | Run state of ZMA Daemon | | frame-daemon | Switch | Run state of ZMF Daemon | +| image | Image | Still image from Monitor | +| videourl | Text | Url to video stream | -## Manual configuration +## Configuration ### Things configuration +#### Bridge Configuration +| Parameter | Optional | Description | +|--------------------------|----------|--------------------------------------------------------------------------------------------| +| host | | Hostname or Ip address of ZoneMinder server | +| protocol | | Protocol used ('http' or 'https'). https can cause issues with certificates | +| user | X | Username to login to ZoneMidner server, if authentication is enabled | +| password | X | Password to login to ZoneMidner server, if authentication is enabled | +| urlSite | X | Path to ZoneMinder site (Default: '/zm') | +| urlApi | X | Path to ZoneMinder API (Default: '/zm/api') | +| portHttp | X | Port to access ZoneMinder site. (Default: 0 (is either 80 or 443 depending on protocol)) | +| portTelnet | X | Port to access ZoneMinder with Telnet (Default: 6802) | +| refreshNormal | X | Refresh rate in seconds for Normal priority (Default: 10) | +| refreshLow | X | Refresh rate in seconds for Low priority (Default: 60) | +| diskUsageRefresh | X | Either 'batch' or 'disabled' (Default: 'disabled') | +| autodiscover | X | Enable / Disable autodiscovery (Default: true) | +| useSpecificUserStreaming | X | Use specific user for streaming (Default: false) | +| streamingUser | X | If 'useSpecificUserStreaming' is true, username must be specified here | +| streamingPassword | X | If 'useSpecificUserStreaming' is true, password must be specified here | + +#### Monitor Configuration +| Parameter | Optional | Description | +|--------------------------|----------|-----------------------------------------------------------------------------------------------| +| id | | Id of the monitor. Must match id in ZoneMinder | +| triggerTimeout | x | Timeout in seconds of events generated from openHAB (Default: 60) | +| eventText | X | Event text of openHAB trigegred events (Default: 'Triggered from openHAB') | +| imageRefreshIdle | X | Refresh rate of image when monitor has no active event (normal, low, disabled) (Default: low) | +| imageRefreshEvent | X | Refresh rate when active event (alarm, normal, low, disabled), (Default: alarm) | +| imageScale | X | Size (scale) of image. Default: 100 | + + +### Things file ``` -Bridge zoneminder:server:ZoneMinderSample [ hostname="192.168.1.55", user="", password="", telnet_port=6802, refresh_interval_disk_usage=1 ] +Bridge zoneminder:server:ZoneMinderSample [ host="192.168.1.55", protocol="http", user="", password="", autodiscover=false, useSpecificUserStreaming=true, streamingUser="", streamingPassword="" ] { - Thing monitor monitor_1 [ monitorId=1, monitorTriggerTimeout=120, monitorEventText="Trigger activated from openHAB" ] + Thing monitor monitor_1 [ monitorId=1, monitorTriggerTimeout=120, monitorEventText="Trigger activated from openHAB", imageRefreshIdle="disabled", imageRefreshEvent="alarm" ] } ``` diff --git a/addons/binding/org.openhab.binding.zoneminder/build.properties b/addons/binding/org.openhab.binding.zoneminder/build.properties index 147def4630562..6b9bd7b2cc28a 100644 --- a/addons/binding/org.openhab.binding.zoneminder/build.properties +++ b/addons/binding/org.openhab.binding.zoneminder/build.properties @@ -7,3 +7,4 @@ bin.includes = META-INF/,\ lib/,\ about.html + diff --git a/addons/binding/org.openhab.binding.zoneminder/lib/zoneminder4j-0.9.7.jar b/addons/binding/org.openhab.binding.zoneminder/lib/zoneminder4j-0.9.7.jar deleted file mode 100644 index 5f202659325c0dc5e8fa6d071437d64af5b1609e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80119 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 diff --git a/addons/binding/org.openhab.binding.zoneminder/lib/zoneminder4j-0.9.8.jar b/addons/binding/org.openhab.binding.zoneminder/lib/zoneminder4j-0.9.8.jar new file mode 100644 index 0000000000000000000000000000000000000000..5343a3a5fd5b772b773eaff8bccf3438b2c8b24d GIT binary patch literal 131554 zcmbTdV|Zr4wk{fUZ0CzR=-9Tcj-7OzFSebIZQHhOb!;17Y;*^w*V_BubN9LDtiA4? zzv_8rjhfFK;~j6kRW<6nEaVqVFj!bvu<%YhIk5l7g9-KpOkP}7m|j{zf>B;rL0Upw zMU_Eb;$s2~EICPDVUP(m_&M1#RJTvmI`DU+XjQx>NsGl<#$vg#WD7!5sd)mn_WXX5 z8YZaQZaZq!SFCbJHS9wCH7`5coZ#;EwEqX}1>AvUj&O^U`qDZ>G9yjOAYw_a1fwmD z_o(6v>@MV>1S>eVec7xZ{z&0P15C9G8rjl$6v0sWx4#U1%tB7@V z83{+!6L=M!L9Xz*SHTL@(v$x0tZX43OL@A+bifi=iMJ^?qU)g9&_~eK1Q0t?$oO3r1~yb*kE{$F3@jK54D9>gXv2en!GeK#wEWj-|9uH& zYiMKo*W~}y1?B(hV(MfCv^H@vwfz^J@&4bPJ?(5wZGg5WrjGx@j`&~NIh#5;|I_$C zAp5`m1mk~iEamKMFJ)?NZ|cZkY;EY|6s-aD!dSub9ZjM88saZ*Zu!<-Ur4UPof z1vav}8f`)_>1vq>4Gq%9i!&Sg6;^GtULv2GR07Pj1*Oz9fh1(;8rhNW*q5NdF4Iq+*>3RiuvV=>fYa^&W3sr2a#6q;yI01V`RGYm@K%gDL#Hu5^D}t$&;V6DyA+*q$#6c=Z300;Xog7ctCtU>p)zv ze~$k;&r;JzNg+}u9u&c}T;m`3N9{!kXUG#OZUY>z?u_e?{&X9H1+C z-szyjX6#z4U}g#r(OT#2Q-KW<*7WBFZ)&1_kttBUh2bkhCIZMlF#=Uxxm+dR*~X+^ z0pNRYbk-n4VJ_-Yb%Nk@yX_U#L)@S|+{Ls-R7zL$pjTANLjY5J(Gi|`sTA7cyLSuJu{?PAau>RA)Y5dG}iMxX+fX0Md5 zVJ)ja(bbII#J&dePkXR#8svOM;cg91JM+acgvzm!dvR!;ksvO)O`#IgcJiVpasx!nPUK^Se$-@2y;vIr-qUu5^LyZ;Cl_%$w-5(0L~IZa8~@hDZJ4M zx#>px@S=Ckz4P zpxfIP9sIT8peZYdgzmUxvsgImcujovXt&H>iJXP-TOc0UWRYjx(W!C)-MSXmNml^~ z^iWVRPQp;;E-26^%2mlLn$yb%PiY#;m%lIi!j!nD?PQ}%C=(n{spgGIYz$yQ2PBs= zHX}_KpB~U$^;?erk;ckR@EpYIYJ_Ppd25RBT!gwwZznQyO7+s1_2Z|nEtsLP$g=Zp z{O$~jn7u!;0UkKf2*fhhbu}sV=k4~i!&XoLhFD(;_#S$P$OIecA}`V-Um{F?pAEpt zlr(7B`N8y*?4Q3tS4u6yCtl*{)x4*~O`y5TAnYjL2Bx}c%TMY^p!XK;cWTPQ7CEMn znS||V+(PQolzR8w=k1_&>AD-K3UlP+@C3b}WpYz*wy?CQ?~lF!!36h?bcf6K&0n~! zB$Dr9wBR>P8USR2W(eDoJ8Dyjx;m7=sPubOoq-9*y*qm$eUa7#`OQ8r~s)R)MvFuw7CAHCOYLV zBW#Lxmaw3n%wk~?-{7SwGMQ+w`Mp{rP9wawwV7laV;NbuLQiN5XNboNo|HBmECnO4 zVe)0;H%(2w%Jdlb@mLZcJ~S-`%j9wM1rk>85be)^u2yF=r)w4_uHU^Fe3Mv1wU}xA zCEu(!8rW%*_nDHGr_G|X5V><1Gqyn%W=)MDnR*%f$XY0jUBP)f-%!xU72S;!`@dE6 zbV`ri4ODcUag6NzW;lO{)8**j_M|;zRXbw-E*xz*&B_4uDV!Wmlm^|J&qK=Ihy?fCy7)hDDhd<{t5)DTG>_Trj=t>AHHA$<)qh9K; z!P6>i#_2f=)!2e}IilUhp_&CBub_^qk6sT223z2ni)Lh$2dC@L=!R^LqTo*qb#u zwl`$!&IM@{%lU&jka$drUZ+b(T?&6d^xpnX*O@U_#=s<ER!Y1@{a%?+PNjF8>ewHZBLq89DodH2@!H5Tn}*!`H>-Q^vhlN&NErObo)0 zmFo{thF+70`3$Va%R;_;{=0s2kw z`}5K%i~aSq8`S8EYUEO=QhZ=2!D(A*P4ausIn)Dq93Oa9!G+2CS#o~+aPE!;hYFCf}A$UkG@5F+=; z=Xu&$vQ@z}l#|f{x@P91Rxd462AC&VBF3L6B+|l?6_X+-f7PX1+D|hai#fh_gPYNV zTC9$!9Al!Qzv}(iIP$J%(--aM!#>b#W7{JsA&sQf}I^SS!j9Zor2miL0w^qN{xKHb<44KeeJM2xI=GE}z*8 zay{GRXE{shq9`@gWFKS?CbpJ%GhQ&(%sq>c@+sX`$onOfjCmc{wimNv6h`DU>4?iA zeFpT0+x!ZpA0g`DVE3$Q4YyW6Ot@nC?Z&^rE;zl^F;rp2<_4$4f|okx8rGps=41dB zNmx$73K30NP?%TxF5@u#C1N?W;9cmqUVG@8UE2Apd4xen{Ve@$y)6C+g?k`i-!diSgpTOZTKnCSiu zt)=FWqq49~*^lEVp#gy_OLMZzqdGD8949BytE140E2I$g zlNJOR*wL4Ng;%Ek9tQjwP53y% zAjS829z(OZF>sPQEt?3hgfx&WjC#pz5o!h8 z*0k^tz(%!jFhUiGu9WgfFawMc42+Rca_Y;ECd&kzG*8V?ZMOL$ zAzXag88D~R{W6AQm=l1#XiaFq2PRFKGGY>JybcrK9r>8wM*1;N|y)EgnlI4i3 zcEJmE136Jr>PMCsjE9>Hm~MX!!=8CB0|EeIbP5hwej7$|tP)j8jQDBQ_Q+Ea*`uWD zG|w7TwTk};nu;)cpvK=NoABSj{bN1$zb6@n{|`a?&yXq0+x=x4pA9Yt^!XDry09!U2XLJR~%mtmLz^xJqu&-(AnRH{ZKYKmg&r29jjgrSy#@xs;vKQyoI z6*uAA?y8@+dT!7|bY*GMn*c12Bj7>x_)lVJ08E)N($Ts@DT-|*7(KN1E_~RK2n{2Q(oeB+t8i0Y;&FnB@` zZ4M$CY2A*a0ncZQdFzRgZF|jDP5`l&d=*QT$6nvv;4I3|BMd)&F*GbpIaT1Xhvc@W z`VbF%d7IV%v@LU5kJRSO=#XFGcAFR-dMNm9>wH>vONYvU-lp*wvBcgk{a2PZ^A_r` zk&C6YGUlYx&=R)`tBVObHqm8-7mJ}fmU|A>lk<)WrzO2L;15e+*n){eg@z!^pb_QX z_8gI54mmdO9@E_rZ935ePHIPD3Tx6f6$L30iP56apQr~NlE|mDB5BG*=HH3B?W&An|#qqx+8@o=A7?*~c-2D-1 z+t0pZI0`_Aa4_2#OPP3#|KvmI6vwDgTP#e54U&$#)Rzg(VKTmwlt%;^V3n|X;sv5+ zOrRoLr48TGZq$>EpWk29!NAIm)JV|$MIhO~q>g_Bg`PJ5nCyh@?f(OXTiRaU1Zv%% z@6+Vz<7VsM)=Dm)gi{~li)=DlW+66UimscaVdb^u5SDFRGpsw;kUBG4_UQA-9wohl5#_9Na%Gb7T}vZ5NKo3QK$-_YbT@~Y{f(6#mp#`i$Yxx#YqY4G zExNpJOJJusfW-(6FHHwbDn z$(65YPA+j`{AWl4;og^ptoUVxOhQYJjp{NilyB9K{bDc+ArpIML{m~rH(ybv@;!K_ zpgt{YcmXksRqzs37F4hZY75>aq*T=m8B)-NHYrUBqTdiy(X@Zei)eq*0E74v%zci? zlMQ^U8uC72O!~N=Mdwi|Wo^)1Kb|ORqyjs-j2h+*%5zpn_Gn!~LHen?4`@QXV5`!< z$j5SyQ7~tfR}zA&tmY+Bcv@uB3>T~0(k8SGZ7J?+mP2r=IzgR@ZdHYEoG4mqf@TH> zX*~6XC_|}js81BHq&MZ9ktj>`ACx$5$ov8|q2UpBtWI2I^H#Toa?HvBgrdJ?%gWb@ zvb1ckn+Gu^n@Y;A=g3+lN>439m@7J8OL5JHRd4R%TSQ8qEWq?B*Ht34m}Izl--a@m zQ(3bE@%6yQ6YDYE4rraKN)}+!&CJj1nU^=WmX+{g?d|c zKTp5311Xyr{DW@WioUi~4`~>|EEFA3e~}GIA7P^mvgyPR)T2Q=U4-MT6{UfNA=`_j z=gmJ|oN97+rnXR{%Lo&%(l#Xp;Y)+?|C}0S_bqoNjvIf|6G0aIrW)Jd6pEl^P9PZSS8 ztyWuqVvQIYNRQJ*=VW2dLK9WOlD{10#)6QVGuuGs>i z6E*&W^%)&b3(O|-C#J)I6{y1^$FUUmI;3rPNM$U~#2&fl zud&vW`}%}d=T}VyqaD3Z(uz|->N@}k>ZtOKEmDXyj1h%oziCYeu#xsJFrfX8@&D0@QXDoqL z5ySL4QN^^VzOkj=%5zo8uPW$I-vy`GPNkEIw8Dktk$TL6E%y`1@d_%(O}g_RXYbDDsnEUkzRicg0m+mSP+;-*H5hx#$wYIxvcrOo$% z=(AE}Zt6&*pgAtTEWWi{(J$jVBm&JJ3Zb>}O>Zji1U$e90M`md5uTwJuBlq^*;j-E z3biO^IhA*~!BZra)QFluRnsBQ|6~G%~lYU4KMo6ncpl4 zTLRgi_bHay$Pw0u{0jN;NKY^CYYlGXAZWBQ5cZ3>H!l4oRZhjJ87*DxUZq2(R705` z-e|qXQQaaDOLgszQ?AFkYcc{JUcGK1C#B(Sx#+V*!;o!ByaYVXL`7n9WT>FGWM`b_ zmZ!qnQ~|ZTI29xh_aJEBh#1#RcZ~F#A49sVRo+vQow5&cyhe}ZkuvdvPNk3%Q=w_C zCvU+Z4szWnoBgt0(`%>#7^-HqOuf(}%j)7u9KF{PKOoWpkrcKoyD089k|ML&oB4q9 z+rYjKW3T(`S3=jHtNK{NGl|KV*${;bBo*(cZ5E*KkD=-8*~4Sy%t0oOIg4oz0*{;X z3#K+TIyUy;PY!@4A(3JROmPl51{3Q@_}#~TDI+NlMYYjt9?_FYiBv5|b14*@Tg}z% zQNvaqqnZ7ANZml$I#6Y#Q7-`bmG$tQ!>6~~)=hV~sh4%Gr^`saU|>+6VZp zR~J(6vE%{0aF@8G9Tc6Vtiv74?#`jh6$(KxdtlMGEAtRq88~M_2XYG>&W(CCcFn)r z(BIu@Drp}!SR;(3Dyr}|0LEUOuz1g1o~pLVx!&JuvMggqmO<-X{k$_V4v;0M%Z+eS zWu0}#Iv9&H(q;7W8OL_idcVEr%NvSu@313I89!u}s^Tqg`BL(b*sOvv&?pmAFlGPh)qyhUsu^X)M3>n6F|@U)Ol`j$ z4xQldRN-*d+o`qtC@1>~g*T>F*~#097ibf=)lki}vShTphp<8D30T- zb>(K##pALY+!rmYcO(`F=}h&^;*LRJuP#y&ik-+g`4EQ#T?m~!tOQMuElJnp8^k z);CG=bEqGBYOF&K9cO+sqd%-IOJ3gd&WRZWahbSkFv&$_Ojit-X^a*Fuae}I?V+!m zZv%O&q>u;DYex?w)k4W`$+P-X*O%=bZ5WCs<$Fk*HQ}dsQA+h_G8T#>UMv%99e5Q~ zxu+#<_||}C?fy1C;m^#8g44P7H*r~+s>_U(zg+gMZYZ(3v1uVfT~vO}c5WXk%6b}` zBo^$Fr}cBv{5rOuzjK31B;+P1QZ2$Ga-1maMgP?T5IT530T~TCaBeJLYE#BehKQerY++I zsL!SA&)oneplpJxj~sF z{W65nVyc!uReeDzFEF6E7U6_*C{C99fO5X^O68|_Qky@?$1RuG8-IuHHM?uqfootx zQO`JO_D1Emad=(CW6Mdf5aCD9S+JuNJZF0oLcX0$nd{z|A(QOUJHs}W?4eR!#QT_X zZ~K~@WWW#o`MuoOl#jr&b@P>_IL7e<*K=)zBFt;~KtNfc?znj-OR^`{@_g|$z%rxhm9HHVeb^bK02o%ULSRNae78Q)IB z+<<}WqDF%#*M=7A)CjNKVHH>qHMNuZ7V6yXpzv_?!u#!0DMoRc2Q#sqAj<7JZcQ`r zeeKYVVs(g)iF^+ek*BRUM z0|3uFza(6B7h3p1uC7q<>}G?T?N;$&Kfw6>b6N@4{n3~>A2l$Bmg4Ob`f6a#L@foH zMc6DC2D}1J;5&EL1GJdrjK`LGS1C1aC^2RAOAA8da0NcLePLqd#NTaK5DjJ(y*gi4 zqq)ToJO~EvqoI=bPgAYc@mIWL@p|wx#E_$r4o96De`c{9K60U2i!g}8hhTI1j9&Zk z^n09XgRWInXk}?5MV)58;8WNurZ1<_*?U>iS&ahAz$vDm;WdTF`wF5_;xHkZ86+JX zN>ThmB0Yk-3_CTp;%BJ(iRdQS5iHC~I2a8E($C}x>ZR1*;E2o0RdYNB=S+IFltL3i z(gvFPT&nhyHolJ1oIMQp$QQKUh5?N_7!(}3`yJ5OgAJ}QT^^w+(b>hg7xw*(8r!_v z8O%yD7@Fg_LUFUO3F}^S94^Hla2%uu~<*tLeeIiJjd!4J!6$@{wN2tX$BK- z7hIuZeN4nPv#t_KLua+qxby$4;TZOIkt#76gfI%A#;)*@!&F-lDAyxZU>ycER4xvN zvuEp#6LGf;VVZ}r<}(A7``eb#E3IND2K|2w@Jwe8^N4<_OEDTpot!1hA&%|mIHRcR zokykJ6;Oue@~b1X1>E586O?FKp)%d(o9nsAEB7rvNe0%KWF#Wdq~1+7RyRSsVVN^< zIRzx-$_!aPbQ;NDJwtGFiW?C|w6H|MBj+Y0L?>?CymJFg+7& zL8L6>#__>Wf>@tj&cZIJ?2Jb`)^wE%U$;kG9?Ox~BVs_mUp~MxtT4yWoSP(g6h0jK zkyJ7bFwiC+FluFRHGTg^$!--9b}FWy#LV(wj$NMm1;Q(*Z`?IAeSs}DEFw}4Ew#Rk z!^vj+>c`OgHz8M(49wb%!|W=B)TIM~#N4B6=?_MZVG0^+-oS8)HL=LatZ0#^{6DDA z?IJDqmmx1qDmLq~Z}l8(bq-12G3nhc#Bu~QvdRDw^XbWDEMMj)4-eQ|!{B_gCvU`C z@%Ocbh-&px;%cSGR>%)sR%Lgb5~F#v^Jx@$|tjbgHEGdo=Xz2TETGbQ2d0L)q_l- z*;!rp@OS2>s5#lkFI$z{3&-$NQXuzhaiEbggNdN#wZY`q+M zwouHBb&6xC5|?8=m)w2_YjGT#35+@U3Z~}+Q6tf*n83(e71f>jF>d^HkOfsGWYh+D zvW{^X-Jk;nQF(w20Mn^fbLC&?G4^)I{CcGCuqqUwYZ8yHi5v(cOG#P4IiVH7b&pDu zhzskr1+fGrFn$B)jh!dHM#10FB1+!|B~fk{lHPJweKm(oI|A~sx5>!7Iw3DaAzB9D zb$!oI;{cl~Sc|BJuV{*Yg^H3PMR`f zf1}N7H+xJ~dpKq2+@El?VkaF1+#joV(;4VRH$b@1Gf*uY$qUjW;}*;Q0ea-kYMesu zCHpjS>;m$Tszb{Kz8BFmdx2C9;%8LMa@0kFQd61q))bLEpnRP5iERWv@e(KHa!<-T zVf;XlUe&Q|cVseIFsCF#3rTgf76d&TRIwtc%cP8#MdgbMtYtwAc}!-mN>Q#00ui%E zi_(7_-XSEAm2rH+_hqD4RwME*CtUBYL}Sp23rMcMl4yEX=Xz;{1EvrU&Mb9H~1MM2}%N&5!v(lZE+2z zx2)|sZ20>IOfM)u_GzveAFGd28|E~z+UYmw_n@;eNZ!X;me}WsX$v|_pM_%@RQMj6 z7tE|GTluxur+c!#&!o7y8}~ARgK>D%v?-(}kLP#Y{MDBqIq+j%n45SOSNfX%NnOF@ zA2O9LJaI5jy|5GrXKF-rr@lOSVp@@2pcmu6UQF|?^JQ4|IE`;=^;*1${{AopIx>0T z6Ja4mob^m13S!}FDDKnE9pIw_p1k9Z(2CM^@d&-=y^9%xgaYoIUfhYi`TQ&=67Qi; zr79V(nfM9Ep1<VP?V`g zKhMo1tNT4ZfOSW;8?;XBee%fF5|(g6c!G3bQ(GZ|yAKoHdqg(ySnP|~QN=uo-}Bjj zczfM&qTj|sJ}|g{SoIy;zrf4xoEP>I7C%FXpMlamd9azdnMTZ8pk;Ejm8<9hE3$5{ zG7}^8@KKM^m!kB0ZvxBD3*3;pi)Z*=X`^389_BCnS0;x&WKaa`h*N2xqCyK_MxJlQ zxY^p*IgcH3NKfDN*H7c_Xq6{n+H~VHfQdAZcXXkuz=$M`ggY7KVq4{vsDlOv(_uFh z+v{Tb0B=T(!;LTxCTQnrHTX^;BOEDf#3@j5d%MsHGGy`lQf%KN^xB~D>Lk89tDk?j z>h#Ke^bNa*4?0n5srQ*q6Y5TDztS=x26y#4Ow&5B-|L_jwT zHONu7Rzjvdg{bKK)UfC$qw$lWw_pX|HLUlQ*P48<$L~tnXjkkY*D3GgoGK|}e_kNBp06BiCz5FVdyjf$6w24g;7wg{&#wecUgOWg3 z_#D5fUr>AJC*sBIC>fdB=@Y=;m2jES;<1Zs`_MZ$z^YC|LTSY2x(;;{!B8w-#u-?0FXXCnFq1m2GXzk86$4)9p%bXJLFr=&c zzdjj9jkOdZ1RNdRU-YThFON=HTFagtLp~j532dL`?Boz3^9-sCXR~Qd1z)tQPB$yb z>X_RXx6oUk!o?ralrk*b} z&o`3ugDV}OwX!IF6a3zyo-f~?Z_MWhb6rq~rBHURhHV09PLVFL1 ze7X4Fqy+tj`S0C5&GKS%U1#_Xa$ePuOY8JJFgArp?!?3jiAVnK*RnTAV4o}$>-zsJ zToC4nvp@iuOSSp^NJMZaP&_ayYC+!~E4I$3((OO+5KO(@;$vRmyXTkE-AM)~a|0>d z^DQ})m}6zm6t8*veqrk+mQ9$CGutFeS)q3? z?7rE^KMVvVE-etDcvU(&A8bHl^H#+Rd!O`gz0se7kM{;FeDXqN=+ z5;O!T1Vb|HuJA__b&o+>w%Rd9TM=8@bZ?BFZjt8*ky@&|2xe3)8`pPTXVKxWzm_JU zXK6?B-!nHTWiFt<2Oa%r2=P_=U50y&mH1~!)y0X%R%XYN0F_mn7^@LEvO*{aFL#0R z_;DW3KtHz(SapRF`Gk?Oh*x>LM2US}%H75*yY(9IqqZH>O5IqvR@0UdffV?Y8F?U} zKXeh^+*mmt<*@Q({lO`~9~Znkbz1n7LW4+$QQ&8$r9ltU zH`gzSr7M!6Hhc^sasdVU>xZ%((Br_<582eJdLE@{fy?d_urS-5v71?FYx8S-Lzgor00e!DX+wfPI|f%&i? zoT?~S%3x$JkkX$Q1HohWuRTKrV;3AC&*R{61q<$7PJUJw1mMkD=yY)B!zh@&D2(&i@#-bY){x#oP+J|;LL>MC{OXsb5r~m77Gp%^IYjfGGE$V#}zIe22qGs6^-C2{4Shs7a#o`sF;U$AZ zylbnJ#dFwsv)6F-I?j;II?k~1x{s02IhJG-%+QMP3HRmK+k&S$cRk*uHPkT9A^Cl? zbWUo;&A^aMV2``9w}j~l(msPj<@(QSRAP)=HF*P7T^d`AKl%3>KP3tJGzGe+{4jD8 zt2y6;Kj~~?YNiuUF>gH_U@~r?g*GFPr-kt3xAaCghV{lapmzpzhPK*h#Z-D_HiN1I zLWv7ayF@l+OQ`ZiX>z@WL^29EzLZ#y_r3(akY1smT?uS%L8G*3JnUbEHHJ{@7fJ8? zS|jmUiu$)KYq{81s@;NRm^bhG0rQFSk26f>-#^za2*AK9 z1^-p?^SA&0B;DA~)(mLQ_Mk6kD?siPMb)NHBeG8Ht;%KZ|-zE5huL;*)i~gOcUphfE_HbAqY>hN0F~Qgy(&1 zY#NRSrv6O4`*~g*F~7|@S+0j(3v|%u8XeGD-lyULV6Riq$x3Fe8<}vU^uW1r=h%EZHcV1BDUedHh)d2|6#~YQHV7YLts4+a%O`$-8 z5oMGl{tfs&F|ZJKnM+d;r)SufAa#t_;xVH{w13)1B8e%A_`Csk$tMG~k;-aQug|}Z zG2K#c4j0m&MTfT>vzKOhjlkOtxnQd4ncAD8CeNySr%H#IX!B*7Nv?bxv%A8Oow6+R zFf-=CL`j=j<_L!1de4%8(}6Yu1(1N_I=~#_5aJ85Y*fu7QLW&l`ZMxJ#;-CN;saOA zTqe0zn_mukk3k)q#vsoL{k?Bo-;(*xCf_k~G}e-kFGDgrD6G5+=m-dA0Whdc#qGTu5ZWi%*@71nE^cSa+LUVS!sM{ z9?C+nQ{>qTz|=^dx#rtMF+?%~jgRCNcP<1*6FRi2zD=vjQRp3yWo@_a!b^^;bE_*j zH3HkNXhBIye3Q3Q%Ds z;Agmi2mmR{zJvAoNuou%%v*zjR!PBrGN4xO7FD(mo5~!zbX%o)%u@v?mk!sZ_EG{U zZ=9-4NNRe+GVI?(GllNUs*DEXw~!X@ulhFUsls|emb+?ORo#pLGlta;q|By_Z2lPH z+X4iNyG^d%-81Qu{Ug&Q>IgNJr5DeaBE|ABhe(NrAc53VbHli8CFie!xR*RY9GFtI z5CySQ!pt=9$Q(iD8(tgIHj$Y&b>~DSln@}hGOyIN(%)URRX8zINkM)rTbb)|@c!*Z z>C|RxAr*J|Yn8MN_8r&h*2|n1$m`tQ5jX*OQi*Ajj`J`v1TmJdHoSRWNCfEhSF+_n|~r$9lYov86~qqIzsGJ~(mfCLwER zH2nOqYHXZw7_^Q2FbG%gp|04|YO)^w{wq81iG{z#AUwf65Iz%8il7Rms2`X!)2nW; zPt1?G)2+lScxP|@MzasW9MKk`BDGJS|19vr5LP>9_*2rqv?O) zCRHny1tCoSjg*q6$RFU~!iW*;FzCWJ!b$;m6)F#q$$(ev^2zr1o)1{K_(%zQbOTX=D)s%?-1&HHf!EeRitr$O%Waw2@Wy%Dg zoy8dpIYZl3^r3Wg^XeV}gt!Nc0s>G2uWAf&=u!FVkxh42+gEDBRyZ8LKoG3sbTu#=j8O zSWGyJ>94{`OWDgEz+olBN+wJ(Nf+I-Eui6r3k+5T0z&E$61j=?n46uq-XL5e6VW5Vizf zspn8=g$~re>%(-WeNYnlfGTr2rRI-W&`E%(uZ6wdyE4b~(#7z_xpw9}SV~(rlWWm*`skMMRaE(S zk$(FV{q|AvHlFx`g7<2vBJe05EvR)6TJcuS&u5c_D?pCM6PLzYXRk}TMui5Xske`a z9F_==FF~KQRXrpAhs>yL2#~;m1OwxN`BzCO{BI|r^#9Zd|7#Z7C@|YXFr#KcIZBd1 ziCegXv$aV}Vg`x&b2!u2Ym^TQf3uGi%~q*?5K6g|wrl z`5BPRi>>=+*~qXOk=WR^;5yZQqwAiZwRI{d*8?d?`a|FM*?wL;uMSR`X9cFc+mSk_181#e+wS} z&+7 z;dGiy6!+{GiC3U@54^f6-3yoi{Nwab=tsDE)14FUXNxOcJp)|`%@JB+?tzL3Xj05I zob(5l`~i&WA)$&KDN!;Oj1le2N#B?Bg$;)8>SQ(^o6atj*g6(!ah=kA${rz$EtK9z zP6&~`Y7=3rpU284$OVIPm33YuYQ1VZ6ko1;NA}00s#%CpV4DX|x!@C}9tRzQcbi=l zzmUkKD)p0z6f;JVfqZ$0L*QTt)o(DokI>(3UCVpD2^DbU;!R0?064 zq?Zz2;UucOJTVDdRLLAY(w@gcW-?lP9!AB(pkmwyOm^640-~hg8okZ% z>j4tuwUjs)r-pcuDD#$nA1LhdUL@{Qv}5<2!>KFU#NvnF_s0&Xgwnf_Y7SRZw%K|P z_PB|19pVNNKOr@!?sNv|yqpRpS`@)frVgkvk}ne0v^&IGp2jtB6h_6MQnOsiQvzLL z!R1?8&Tb!e(AYz%V$V2@;TUySCjXgGG!h58MSlU2{5Pro0gC_qrA6}p9u$^;%a;Fz zYX3KG4E}QjRSTf4`Ts4^oi@Z!N1yMkCCeAuILP<%10Y&d3IyVA;@EcE;EKwCWrU*s zt`aSbS5R8;DOmx&G->&Rh>R~_-c%uap#8B@)`PBi@7B)GKU-JZTAy!!R(8N{(D-sg zn4!g2uIv;42*Q;fIjwku+Pla$v9B}0pCRuTpLH9bMA|NaX+N0!EOR$Xj5}@ar4N!g zA)$eSbwN43*E#NpSDJ;VYJbo&5wUZhHiews^^RO zID*$x%^D{<2V93_w|V-+l`QWzT|KkD*=v#Y zjM>^n4hl|d#&c!{SY%mwUWHpccjVIr{UNjIMn@4ze$ZOsJ=hYm_glOmd?I7=?P~o* z&6Rv$gQ!rsTwx_bHqT(R-`Eu1d$oY<7UC+Qcmsn}QJZtEN7UPVKZ)?LYN{5y%iKtk z8MT_=E5g&8vy_BY(HX$%=YYVDaDm3asvTZnLgqwrhuoF}4{}2s8?-`XNwI$wu3gyO z(pF7?LZHa*h-M$D&&ukf-}hCQgh1R#p;W?wFPh99nMq0Uc1O#4ZlbPscMg;P@@ z(Viw=w&nYIgi5^@&~!!!Y1S{~CwP_1Xt62n3T;|lW@U$HG0{UYjs$tUi}(At&yMa_ zkc{I!l?LGA{t>N_a~0Hdl>!b(lT;G?+QF=i48sl=XX%B@D^j@3;Ti^OGwOd{hq}bU zz-eS-Z25&f`1|T7`CBo#*2H5Hv4UY?VKp{y>ES~&IJe`(OHfOhEX})dm%Y;5i!cM_ zw_0EC@^*&QaqLAk3l-k83$@g*@o&5NBQ_I3^7ze%gd)`C_;q5`Us)>sT5(UCogBL3 zHlCsB5;ONk5#1}*yY*YgpT9_#4kp#0@m2l!#r5%CY#4>i8dKts)IW##q43I;J`&E% z<0O4h#kLQU#ycrO)8PaDk|N{Kns?X3YvftOpHEu=IL(J?Pze~I_%T)}cz~LUR5TMt zOtT%h*^ce~TiFG62IjCc0H$>hlwEp8wLgxYzwhdK>mxWt62seLD8KXslabwpJn#B= zv%Y}AD~}Ixu6>jB6S{5f8B^ubih@#n3ewnXLEM(OX1-&n?g`wd5aG#sM>Sp+3HCmu zz&h1Xbqx`)!b4SoYOjQ9M|gH7DS>Z@Va68~D!?A(g%%u1AUUTQE7gvR-NPUp>wv!l zGj0j>7rXprA3VcGl1U?_g>VOU#QwK6#xQb$1RfrP>>JyMgBRBT-%txx-X}^Zr3K{? z#SHUzK;sEib&U*utV4YIF^%8z7v&_9xGKaD3CYE4D}j8Nd&EgeP@IIHu$5si6I*3gnj>hXJ$8gJR=Gj@Ha7EB9ZIQKBpL(MymEE$PW2!bpyx z$E*u=a-F7pn}Q}tia}_R8z=*6JL=dm!+9hBrob50Za-F%NZko`{$lB!5ia(BF2=t z@uZx{l`7VYlJI#WXYM;`E@n!m=(}uX=uIS!b4E-(t}mBgswUPH141 zr+=mhS|txA73n232OoSB<>H^i5A9F~N0jyf+>5pK+w{CL7#>XOh_ha70R~~~)1j=M z7fI!R6L^{K=Y_uD9w zdFgL$*Dh&+&q_^Fi1 zy$tnoT@5km3SF>yM_}_y1QQp z;I`N@(ei+RF=YsT#4R?RxlA+d9w1NJA1g#nYR82nrieJAeq-^eGnRrtn zm+X}OCE2n4n`EcrZ0PLbByDT@bBEGM$ z_c#E!y#nBi=$Vvpgkm?kq)vh1dKST@%1+}gB1TWW<^x1j*0#E3&27LkU~j=K2;5&N ze^nPlXKH}i?2*8*4yiEIr72$#fq7LsnV5tlS^Q+~A(OO-m#Q4LXUslI9MT+uyk6cQ zEn|$KPaup_W9K82wOnXJkcfsk|Cqafy$)oR#YNVhnubb4`1M&P<1v~>$B{*6&N=3A z%>#S`0BRY;$Ce5{@ZcOcjlx%Ci^&c%CWHvwWn5{vAkO1M|Jb??H6s$mN-D>Ugi}jt z1cY$~HW$d~FGjSBQf;&ix^(L^6g}92&pmj}d#WGI#X927*nvJOhM=lbTEc=GUd#0} zioylT{Vt>+KEuh5De3Cds-|}OWEcUYNs_fhv3qMu>vfCLE4S$tIN2oEWRNhKi_k5D zKAiy;e1>vkw*1s$e7=PFBn+{*?xW)?5%x72ynD3V9s3qU}5llf&6d)NN1W=VpMB!*O_x%&>#7N`P+aG zG^I!Mc9YG$o$O$OD3-@dPISF~N7r};2r zpED5$(8@N+$q6@oNe{8xA&Vhs7&qpNrTk_5JA3nst)It|W17{06 z+yBZlw&bu?kcXx&>#2dE+rNJ0JDwViS*=B_T$L>1hjxXAfUaz6glkV(@lR`AYpv%` zf6NVoXT1_Y!n2+Qm+_wN@5jV4!g~dNEr>jr+%(uZ$B3e9XL|7JI`BQ`I5>a5d)VUx z)Q38E7K{Ka0bVmhMdGSbL#3`czyA7UD{q>I!th07EdYFx8oo)p1a&oTEKt^*Dc5do zvyo;B4%^DZDIvt5On7S#_|v%ON1Qesh+8ZTCLqcARaf2{s`DAyjoj1;C~q|;|G z8z$1-%?4~0%Cu5A1cL)>R%z+3nbbV2Z2e+8>o3Qof|iu7pC_#kT|3lQ2gpOP29@@m z`f9qF!9cVV?GbnJ`)PQVivgE)W0&PuizUM7F_JDSCY-7{kx3p?aER#$Vz}gr(8gUB z;UmL#YW{a0Z(9e4wdfn$ZP7WfhUh3e{3U9-dYFb@2c+zv5}0As^{+GwuQJkxMy2}_ z9wYax))hou`kl!C*@J&kk@`GCxfsOViTuy<2bwB4@c+x8q=LH%~#9f1I zq*{xpsBp@q$0khL&Zw&?lzA{7t00XBeh)0Z*PlPmV}-%(ks4HZ)?+gO;H*)A73+*s z^qvdqK=$gNaTYQXX7vcEIPTPjcH3vZg`}ZNCX^N>2+XKj5|g%lsDjk+NRDJzAH3iF zY4CCq#~#3FzZt}fu}l)Ll1UVqIu{}{NSjViZgi8cA;vx6n5$n&KQo^9g+{eTNU+*~ zvCJ{T1Sw=2G7_QTM!XfVfLd*mG=E>da*re-ZAs=(uGoyH%f0U78r&4ru0HayJIHl~ zaOJ}or`l|PTOnHTZIvuJM*2{;ZB-ryeQ>xOn+^p&Ld-UY*(RNyTBeVX1R~~Q?&L{y z^XVtzSYk9Jk%XBu4%Z$VO#V8KUsd0zUUK@v| zO0Fh)?SOx*k@;yIUOMuMmH8;bq(?eoL|-*N zhrU5rKt_t!6E?V*=6(m|3FQ!y=b1Q)pL1Mq&+)&->`!c%cJbnmOTGi zksgb5kca)8wuO%HJGqnn{}1B+cjT&-?c;~>v7G}E%tP@8#GJOv-`c>dJF^%mw?{LyNK%OwMAxpFDsrodKY*AsHtS=UuYw3)g4}lT~_K8=oamX zij7OSn{`zxSu8Ck0@+xFVJ0dB+%rHopxeF`LpXl7_oe@Cy7fo!B0fbe-~W72 zO1}+W|7phfj||}N$d$Nd`&o(~oZ_s_V#nfg(Mc#KXvyP`Po@rTXkdnAu@QucK3R-j z6Vz&}Ex8BrVsl^2D+NJBQ{Dw4rrqY?2O@Cj0lt(EWjeXaiJT&IsIPbGy2yBnI;i+~ zKbZOfpbx8#EC^P?=^CLS?LC>n(t~}{dQyYxW?5n5A??1{Wsg(7>Lj8 z=ViJny-BIr_TsF*%#6R+>c%sxaiG}ff<@_kxXv%Tgs|fTb+OtO08Sv==0Rdd6`OL z&B19AtU}=FM^3)1ob}>b`9i&Rg$cuub6tkVfZs@^ZrXrdAJOXTNU1e$^2|hW=Le;C zJcZ~wXWQ(0YSyt}VS^$CmN`5sDVoISOS6c{D2c>{`%2dhJ4MG|s0WYmWPN8d1kgxo45ebz z*@_y>sK&UL!yQ&G@c@p>S<|aRC;3}q%7yBg>A)aYUn^y_7i{#ytLpnlBlf0p-DF4I zrhaMB{tRjc$a;4Wy)ZpV=zM9>Of0uDOtXA5*;0#f0L4;hnD5G0W=~hK7oq3sbcB@m z27DSYX(QG(J2Z?|+uq1X=?H_O3+4XVG!_DmpV;1pvmPC3g+6mZ$``L1ol8HDht4>V zW;+*L4su%yorZ!|vYlGaNM}}T*pJ$bdNZA6!gWzzpl~=osM|@$rC6wq2fns&_vJDb z?fXMQ&my7?5d)J>pRj#*=3yit(m@hLxndI4aji(SK|(FUM%mE(kXqTTLqiID*Gzf? zFk@f}`G}Z9h~EgTC!a)Q&j9d14edcj6|AEX9k3u7q+rPF$B^XC>nA43RyGVrl%rsf z38SNMNJf;Sa)?HpB6r9})KlFBBI(NShat&U*(D_DI*u^25BMN^^u{qmjRuR}N#fg% z(tA5)8o0AMWO-y{>TJ;ZnW#A)+bEY4wCQ%XR52~fxX zLg^;+SS`+BqH%6G011j%c!^jWoPJUP-h6>37le?s5t3ZPb*fIq^5cx*efxB~c*{x5m}V_E-1@z^>zN8tcv^Uu#~6w@v^fHXDy2l3b! z&S5sOq$;y9n+$GrGl{NkYJCe1X*=PUmZweb+|Mvj$ z_ccC~b|*V)lh5%CCV$Lk{0sal6-R8584!GuG}4)Qh>Z>OddnpEnZ@zp0%PTcWhV$p z(%Ta#SiqWjEWHSO^m+)yvHvKODV$?~5ORPBc;0pMf|9iK(h& z*87!A4A)Qav5zG+5mnuE^Zp!6D(cUe^fY^1aUZWu&XsYu=QiIN&7l{32y5cxCTVy+ z5fb#8i((gnzQN!;L_~$MdD<^r-S-qb!z86fv6V~9D|6wVia)y)_3GW*&6$TU>{N!B zdKn^)#emv0V3y*LtO zK*`F6YJnI5h(whn6bu4ogEU>fMum@hB-Iwn)rmDj^Nr%&-?vKaC@*{O zs-CZxZ`%*yf_kUOVlwacr?J_uR;Gv6sj=~`51tPsez+|wbc9sb3Y@g0vv02To5`~G zW=nBgO-d=cizzmIYtC4(lN0*W2&m{j`aE@YGGi9TV{7on6KJ8DmC4Fxt3eY07muz? zR;xCAaxIo@*kc#&in+_cbm`U9xzjH3dV~kt`De@T@%-dztH==5EiR=*K?cAIhnS)u zlXzZ~?%8(2_Y^C<&&Q_{UbJy05=a_x(J{dg| z9c`L$i5OJ#C*RsYZAtZrR_9G&&>STCw1BSA63n)3v`wKZJazQBE$&rGbK1!0jd7lG z{9qpnOOk@Q0$r7O<0oDrIaIdlvqaevC4iT%aD!L6ZmGA5YQl_|Efis>gt*{>P2E_(_3MlM;#A;z;fOmQ&AFr}2iz^VS8P1>0B&Oj<-#4?KZ$dB2NT+b1dJ={$cbqKzm zePMU_3+UHiX$mHm&WKZ_L+ChRV>BnIHj2x!cRsn1IQ#>adpB}@LpJb{$Uap{VX1hjEL>D2oACQpV`r z7csre$i$yNQ#kK;IqTgONX65-Kb&H!NtxC$zM!Oq|*ZzkGXMNrTcHeE<3m<`?mt2|E%*kTsKL8qaaz zQ9b3w_4LrU^#PFDV+70Ke^aHBQVf&5B1;lm_4MsvwCzl_&@O}#LT0FZL7Z+iI@6B1 z5Yb#5LfS7|XS2L<0t5_K?Z|WhDhC4QAV+hGX0DM(i9u!{3n{eH4g%X^guHFDLbZsF z>cNLAk$3+s#bybv8VXyoIT3rTGHqbGd1F<(m^hcNgM3WX+{-rCAgBAPi7H8;2O6!j zSnKG}937E;PPO#_Y(li}Mo2@$eoQx*mn$783`bz+h29>_EG+p7Oo156a=OjPgGm9~Z;zp7Ui#fvHutlmlwA%_+ zYS0K`ur3vZ8U6;52#$qxIaCfB`jP*=Kyjf#NZlq={vHr5<&9K@Xd9C_7jPoQQ7Y)z zqfonQ;zrlpQJw!-S-lW!2vkI#if_}gmWhPyrmIPwhF;2hS>t(Z!l;+Y7==bx^tK^5z3EZH-)4?0#|Y%4(jvOF_MO`!m0e0NJ-|9z|u}{PE<< zLAYb~9SG_|J|q@5PP!Z^v#KR9%VD=9SCNIkzR6{3=nvm7hfq%OTY6#3CAE_3e0ieH0ALH&r zzk6tx3=wGe!YVRoVmyXDJ4hlXZV|ggq!Weu6e?&3PTL3QxlEFpdtvci7+2cXD@@{A zB;6dcCMzrUX{poxtlLA%Ah+H{BXzt`QD#o!r&OF^s2QPGVi(1SvToYX2X;mNVHib# ze1{KYh43i!DjEv(L+l;b2j~v~KwxPs&G-ZW?%%r5{sTVzHvoL51JWk0|6MUo)KNlI zKp*-T3f=%!Bc`nIrWjM47WSeWR_-?xOf^UfIkFr;s-%$uj$m?KdN|7ePFh}i)CRwB zo4J?#YZ-LBX6lLOAoJtKaq@ghPp=EO#a{*12d2JiVVh#p*MLjQ=Db*!MQ5{SgJx6G zqvNb7)3j4|Pq1Ehn`?arK|j=5i%WI1cHNWmz!ejw=k8jOb%>`cV*Y>`EJ)t*G)N@+G{_;) zB-S)&5QsK20wMyc{R+Q2|BtT}z;E&cr~HKg}tRI$I<)F>@qqzlDF%_^yk~z zA?+|TLXf9TJe%~fq9H(^sZXPpRo8maXEv&_y8erWE{kt42@O1dq(gEFT{XItcE+n1 z3Ke#J2(fRXFF<3+vUzSuOt>s)z%^%^?yQobXiGnlN_ZokAvlC;E(;;g#1L zN(ClY>0yrjG0&tpC)zYItQ00}S^OZej(lMJAc@W_P&_HI&J2n~g2EkE?d$+XJ?5d| zas^mBI#anN+McP(iD7TWv5s{5MVvdCQdaQ+ItmyF`3An|_aNdc97)e4DD4u`@!_Oh zx#}XlL85E$Va$=EIJuT|?6=TX;DJ!<4FC&~?-YTG1hgzL+6gjG7YmVxgXxSN-`1GyV!qe@ zFbO_XrG3PdxDH*hk0g1x+y-C%c(^mr;H_7WV}1CuRHAEJ8vOb>4sZEWy!+q7E9?JD zvMp+0^yyFZ_*-w1myzoGV>n|>Vc5cA8DF;+)_|| zTV?r6@W+CF32KP!Q|!%*v@Euctf}f|cz^@;%NYcA7E9UqQL(HN5{xHn*oj7o(%5K8 zGN%la%pq2+^8OXWqXLrYoKi9pClf{wrYwbBkPc{@hIV9g(Y`SLlhI=2{IJ{zR&IaY z3RoSwUrLnc^UP%a0@g@La>{w-o*UM!;5ty50~yblkRTYXE$25s@^?Pe=%c((p|I@; zA!o$!5jIyv;-=Nr?!A75$d+O4Ab5XKp4pD1>p65HNdt9yF%O=!dc$V)bg2)c89bi? zxM6krigUZT8(LC+*iqp3qj!5hX0!ZmA+lRH>&y@M4`^HLqdvm;WI7MOmHPgJI@Ld* z?cWHm@`NLj7{dFYrp3OAO^`M4FFHa7^`^Kb5TpQUeEidXfx0~5R>=X23LGiumx5KC zR&iO6NNtXNNm^7CN{_D{;C2FlJVXW%i-2miv*)+Yej9Hx596;7zb^a$srUTDzJ!bs zzy}eFP?C}u_&v2tB<*0!FeP`qm!J^G*J3JEV--1qa|xy;6E zQs`C6@G{oHJ+}kV9aB0XhPBWBhB||~%bFV1<2cIc?1T2ZN_SbbMJTnwkkV>JM&dzm%}s(bYJbR?iS{1L20p86PPaAjZK+-*NJ$R1txzM-P5ictl> zLK0J!gF9i+fIiz5K33zr%sXuhiFkbZ{T8V1CRYd(h@rwaEPHmrppw@nJo+}Do}pIY zZI+zQagptTCa6qoqlIFTyRp*|Zv!|2$~LnkyeJnpkhsnN&}w!(FK#9^`-0#F4U(wl zFk@zvI(mBP9M~Lt&b92h`w1Q@t=Pde&-yhifiBJ})5qT%o~$H_Cv&q;%|u9IE7Rlt zexcziGnWGAy~VZOf>pG1&>?*ggR@cWR;5A+ZkECVRYb-hIr<3ui~KA%>M)mxZpyG_ zuJEctWQXWre?Kt=>fJ2z*p>xO2-GX0RMcxT4~e$Tt-THWmfcS~<9C6wILZaA!Pj0~ zTT`cCT-znABdI&I@7!C}p+!h?vv!hmnHNLrC9RPxuZ0%0duI8}g2OPEP+_QLa=P zlf_~{<_U?Vm{RP;Ql*x!JNmBZrV$)I9lAsq0Lo?d%hK5XbTK;&YV92bnl~3uE#h*o9Z5T9KxHSPLAM-tH?->)Vn6+d9?p$?|uGSgrJm8t#Czuk^LVUM(oAv`c0cHUzTq*ro?3L4%ac0R?P_L zF57+Y%ld^xWM6dB)f23;qnO;3N~CAKvu1C;h`MUmJJ8hFzf$GnwYI_6klWKQo}0^r z&C{e9`)oc6!qiaPt6y@9q$wTwxC@44&Ts7ag}o@`s@f;Pa-|dn%K?ea+02EaN5AQD z_!!JJO-R4y6V+s;S&;Bn{xB=OmkiU?{78vtpU7kx$-*2?TwN3ykFTP;bc&Bs%#x8D z`PtM3ivhVWx@eScY+@9Uw2SF%Gg#3yOI*XZzv?&HtP1*AGyH?{M*xR%+>a0dJoFqh zXFwEODfJ%8J*)31Cnw9~zhtaoJy5dM&{qa!F?MRlu3+xp|0JI#s}(}=pWs~bFH+RM zzjp55M#q18?VqBgt%=beHUe=2TVw10k|hUJ))bKh5Y7aM5vnW!M1-%Pbk#*bSdb+t zAW&1pd!gY~X);WiYSLEHvqB$#i9OOK!~1^4Gg;wETW{&lulaNqyR~`A=I}bb+`SCR z0i*`3fHcsjqS}7JiM)4@jge>9E9@pj^H83=M)j#qzhF~@xMfZ;96@;kPyy1!xIJsj z(I&GGzNoOfP|Oslj^QjQOwNW`gh&&xwDvAaPFVyIf zl6L0ON&@y=(=|nJ&^b_MKq3bUkaFMJmRjlTXRjH2pZYL4;2Vr+*_8_0a$H&H(?nE~ zC1N04Kqh=^PsC1h_>87HecwEsWi`K4fx;ndq#zK9t=6qdLXYi=OYxEn8juo4$!SUE(`ZkqBM@y9dN)Q8)z$JocQxHD%LB-k z!R>`3dPCeSi1)FKlRTi{MZ+9EvMV%OE9oQ+{6r%SX)mZ9u4aFb+AT2bAiP0 zBS%ofuD-Vdm%2lW!h%IJjsC98bs^1P3}DrA&EqIvSxV7@+Hv~NV6PA9DX@<6#fQuL z0CD$`AC5@X;jykcJ&f&I-RIZ7qIty=Y-m5z*rs@c@%)OvoT)+%Y!vc387DK3@98}< zDLwh&3dW1O`S5Yy!1TD-(e1~@a>h)Nlea_w$OL05OJ;gY6iDZ9=5dQET)8-yuuD@dyecA=!$}V;D;AR@^iYx1@s}a{JJ7m_NtHzqm8_(js z=9<|VXp4AD3PfX)bvQS$7`z?V_-fkI!rs9Aa!BkCgRuZ|Xw+4wgCo-}mP~S?rYm+MFWBB!B|M zkemEhs2w&*&mqC0QHWIfFh7HvC1$CCGtw&4G+2Do|Js=KE9>HdD%Y{u=NvxI(sCEC zZx1i<039I*wBziTk~OHF->2NC=gB+t6!jw_NGYqGg-<%&-vpriVf%{6!;Fr4D}cvJ zPPuM?NWCwt-V#}H)URih6PfD>0)6sb^jUtWobSO!?)Glsg5fp^0d)~?8jf4%Bt4Q@{P`+F1n{yTFw48T)+WEzR4k=;Y-#Cx(E+po>0ep%wlG61)Rzih=@K4lZ8OmAJ*}sPBI- zv<&n|5 zus6gR)%oDoRXt>C>x!@sxO%*C1((T*mfJp;y|dW=-T4bGx1+upXW3L1Anx`s*QG0G zH05z@F*LZT`TN(GfHd)$!|RBQ>t6v=C&*D2UV5h=%@8W>y~qZzK6G4ro)Qm!_(Tic za@@JSCrDdiXsj-ifaw%9KTtebY5TC35Bh?sYO~~CkqeF5W0wbreOYRyYNWZBLQRq@ zOf+R;F^03(MxykWMCsYKO*4I{YMYU~$V!`Kn{sLe;=F1gnH-2kYF{o9RJR$dLEZJZTN2M^#Bs_ghMW{4@gjQm5b{?fb%gKI}bhkyzr$q^x2Ee?@Edm5vC5{aXD zbw6pd65Zasw2y?sxfN?iQj7w>kYo-rveoG^d+vk8(@X-Kt?$G^JF1W{E@dsXMAEwX z7;MHPO;Pzs);8D`~fx2{=aKJV&HeE__ERjo>Z6WQj z!Rj7Gd$=JOg6+{-+seraGdBVr$-G)!BhXOvk+Q-r{q&N~bIA>_7$HZ{hgA*6584Nz z7t4egYY;@y4zNZrS!RNzME-0Tt3~XR5>p4W$YyJT3kt9g7^Lqmzd{)Nzj1E%5<$g~ zSxUIfYH5<=VnN+TM@vXE+|K)3(iA@g%&8^IoVdfxmJq4Ct^&3fM4{wPPq@nsQD@8Z zm!^Oon6=E`!=RQ|iwu-mw==J(1QIWopdF^&*N?DgwX=sU_Q3 z7rum8jJ%bKar5qy|xxLIcF_ihL_?(`LgF<`2R{nzlV) zceYomelgF@2ppa;+vd4s9TqDfdD;W(QzfZfE-RbgvI+a~ADB)}OvjjSTI0I=ca57$ zao4YKst#t2Lw8`BZ5jML5VyjxdkqLHl_zGF9}E;H*BxAoL&%HgL5h>dxJYJrU;!h_ zn_=Ai0P9rJkg{_>oh8M=qTJM?Yg0Fxt?#Oyp_jGvCfCu04iq#4*<*lmy* z>tYA4dFvLNzZ59@D%=7%i zjU*DlAhXEQ_LfMa=Ump8W-!z6Wi-%q7ihD6(9nv)>;r? zza@y*x3AMRWD_$M{^o6^#vZ=oDe@hqcET!q6+&>JQ_-e;bOEl<+_i4Hj;?e%DG4;B zYd@D7QJk^=v~7xB+rCJ>`bmq>d!(7Whh7l^``qSwdSPl}U`92QOr;rv*q;C_!rWTd zMPk#DyajR0Ke2EEme{EFR07tl>Sqxm%9r=CS&vlS`B8<(5y6ME(dPT|q&LhIlNf9a z#a6fI@5hb9y(%F|=hC~wb!QOrJxjnz37%x5L&~UC3_m@M%Sv$wl1}X3@}rJ9`xNbz z<${q39#+{IVfKox70KI&JJ0ZMMV=NS1T!XJLs~a?f9~TxtY1T^t@oC|#CSRR5#&=Q z=`>h8MjAA6ja127Zuu5hS5}UXo#6*NtV{1fuN#K)ITn>CqrX4mWnIW*?VK@_UZS`( z&WUFa3cFsz3gu&hQQ8B^O?^`ulr$s@o##|h$(Y60R4N;_ze{AFhg;n)x;)H`b)iAr zi6e^1&k3j*w7y+*%3OI61#xqu zugD2Y@_Z#LTht6{Ouf)a{5S$|B4}IO_pSF@u4#z_$r36z@){nz{7L=|=(jMUFyA&sy}3D>wi7vP z;;U@ilvqYm&nrx@aj0PepYT5YlD;7uxV{$YL#Q1BPp$xk$Q`1($Nt#YFoArKx^L)y zJS;wDq;n}pQ~gY;q;V3air|mlzW7X+&|R+>SL5AY#Y6+rgxz_dx(y};B#-eKLcoNwyvgm1B#%MBmDIL3*{1 z*VjP$$q8vHN1{G3N7{pEQtBj!V;Dv0^VKX1#wo<0!eGI8`n&n@P>tzF-Blw}0V?CIo6Ju`mQV zm|>Ew6I(Wxo7ujr+_YHzFz}=NuC{OkfE@;$8^EM}YF?53=wL$eqrT3U zusbRJ>tyJ%TWgA^JJy__a8%?1*h~YlydSJ`qFjANT^f}Xa!_I4Iba)9h{VA&u)k)B0mcEdtSPqGoDB?9nXT@t@lK-&chNWWa=$OP4~ac zo5P1l1#c40tH`D-j+7;A+2OI6xgiS&4z6B@>o`w$5ch4ey3Qm-_jWy^!twsevPzCb z@(w;TQM-SUssH^^{vMV8=~4bQAL{S&K%$P_96fSK%$T}5TT^fDzLU2Q6|rL=zdW;Y z8ysn)vLyvt`%QZNxQ(G&lM*0b6tph@ACyCUu@-tFMQiQeO;=2ekM+mv-5WRn_qTLl zP%t?X_!$u+BMT*ZqyAGr}< z0W0(5wo2ooZf4OC%~WxE?IH%UmR?BeWfbrU%q9)kj`mS@HyG#R_tUEILN^%4+w+k$jt)bi9nb=sKDkYbRK3TO*~V4lhsD=SRRgj|4UbMHk_8h+@z_^ z(9{>;@s+|bT-j^{kdXs;t z8w>*X`RsO%4`pCMHc8e*$M;g&kQ?n19}w3O+GAY#0JC9`T!(fFgyvgTncuv?z}D)7VM4}b@XcL3btO_ouv zXtnJx7UZ`T@0O!94!q0OOSJk=7H+vM-crH z#-~8l1H5dx$Lh+eiH?8_J21Jz4I`oLHGxe1+GHBIuaBcc{_!ueSmeqt-K&?ERBZZ)OBnV0+g!;Jed?$`% zbZVfS;(%ksx{{R2Kuk`ssTckwnOFgvvRmy$Byn9&TTIrw9jjtXFP{#%_p3%@W1a*- z^Lvc88Tdx}cKb!%!=Gv;WKQ5={pXDX{C1!I$7;wQhf4nqd3v$}fBWYWCTx)y5QaW% zrBZySQK5pb{T`=Q@Qg zHAbv1#!si1{Jh>@-|n!wrRE98Av@?PNhIC{$$aEQ-)lk?ZJvJ`-w;>Cgx_)fQb_4o zTFmqfC8CC~hFM@EK{5fW^I*g(N;(DZBNz;6WKsW{|0f@S`GUF1kMav zFvb5`GA$gjM?qi%&3qk(rIbwEy$T#v zYsX+-hvmekulTsp6R)4&=x`JtC7NNHw$I1Mq+@#mD2vi_MJKNmIqRS!K_%L98A3JG zC>{TjX_c6rGoobKgmvJ-)5Xr@H)yQE%4y}BJUYU4(M>$5{fhq#_9xNaEA#2V}e;EcAMt^sIQ^HE4o@p zI)t#uGfQ>BR4Z^;ND^$#2CIijhI{(LW=)$h`0^%XO%A59W7noQi%tVpa%-5?BZ#Cj zWgS5^xgDlkL2PF_gV`5}ahfX}^Lc0|<31g%HPw!!ULbX#*$Cybo&}6I+y0m4U=&MB z`)C1XbaSu4Zsg>YCrQ|8X~|qFngmx-8x5zTDd+ z^NlUe6JLi3R(j+q1BP{uV^M~^@zLLW5bENkrC%;%|0v(!a*r?Py}@^4u1BS%dQ{BW zi{3rGOoOmMzK#s}&2NLD6kpn>0ly?^a^7Cvf~1pILQEc)$8uR+LtcO!@kCGJvn#Iib0|Dut!IrDY8qH7VN!X z^Iw!#DlMCP&_4|TZJo^sGgMkw49;D5*Ku07SO>lA{iv7n(Dw*vgkJ% znQGquw5{i#Av_m;Mv~pXorQl*vunmNVd-ZWO)Dg zU;ll6`p2*Tbp?9^BP#*EZVi`-;6nTy`3AbRZB`!2e1*yhmfe%sjTAX1PD5Rf zjPEbT^f^jvRB@~yj`P^J>l_{coc&v54Vy)hi@6{*I7=7F9IJXMj})3ZSz{d6Rg`iY ztg8f^Q*T+a1beuOcBa)G5dmM3)x{lpKH}oX&^eYJKraj*L_`b|^`Bun5gzEs>+|-b zKL7rACja|b^pD&72dDotUjH47KCN4yllwyCpx%hX4F#^SL_mcJfqebVX+)-*&``_w zBGIJki_1r76RfHFs%J6GGOv2^wqqQ*Gi^8j^>DGu=DO@DWH#c`w#FR<+Qu7O6gtIB(q`+{DT!t9X5(Y+hxAZahgQ(vFK^p>W(v5?M=U zN9lav`#K7q&}qa6zok;dm)!*VLzCb>4N)i!t8Ie3^&_d-`?k926u^~>QAxe{ni*r8 zwgn^FCl`(Xq%mORD9f9k`xSg?bXq{%Ry_$Z;13 zbykc`o1#~|i_mnk7)ciBr?i!VgbYyV?6ues7+y%)NHQT5Db~o+{HTFVbMhJ!AzgoL zARe5u*U^x$7Hy=;8UePnRS#c%Hs~@w>78qWR}uqk#4?s+YF}tkhOs={uC0ls%~|_F5Hprcrk*v-$&76rSTyPBnae6jT1)pSkQC<6L*J9oNf~-lV5{L7 zoai;nI3IvJ&Z)nW)kMz(lw6*MK5-#;!KH3tOo$YC7)`(Sm~S$t z>%pXBiQY8LygE*gNB(l`D7*d>n$$*%%9lQ&sqk;lpAvLbgIw$@>MAob`Gd;&OV`CqSKjT!xt4}z zClFpQ-eSi?*UhIF%){yNp3Alq7NCjZ3jLOx-qzA34s|l#HWA05I7XxR`-!Q>vPETH z$w_i)dvX?U=#B;*_la0tdiel;es^-aZEmE?Y9huMY8Q1`_u>bBhQ*0lGktI+2Gs4L z38-Rg-)dblD|3B1ZA+7_>JZ8@{T8!MCoQUr7#*fzFEfprlgpSh@^U7X#+~y*U0H~@ z8qS!sl@+@VLQ@sSGlRlr1W|ACb23W-MNK`%>1l;uc^2nt4R&eRaCMJiwOT)p0Ufl~ z@m_E;hJzYb-DH4TKb1^9v6g^So5^5M>CR=Xt2Jd!e26V=7~=%EuN)&9RKEjinaWNY zG!2s`>X;{Fd${RHXp$wnr#}+L7>H_v;fl+Ih*5gv&+Ddj>pAn#2E|gAc z7oV3mMolEoM$!XQcnk*Yl7+;2tg(iQ5L1Yyq?-)*iXfAd-rG5l&Q@2u8fxb`l%~8S zdbgP=*XOcM`C+qQrMf#p)}l$b7GM!AxB+CxZr}tP*$(5hs>+eDa5GI)JDFdO7r0MY zcVaQ3A(27qgZ}$A3NtbJYzsRWnhNLws>F_Gy7NY8d2!G%bdLC;xic3cP%ZV-la(ud zo;u}Fd{&I8wpuU~&cTRO={k0-Kz4rZY|AylZ1deyZqE|yNw#A6cu>VtdIU?h^x~4Br96Spj##|D+m2YC#jV&A z=cT3B9v@CRcYWOyZfC=+@qy)L5>ef5Vc_f$DR4RaP8L2HA9vYRu`ko$MU>+HdV0_ zhzu{=eD7sY<&K^LAXnNWZB|4aMmlP;w#G6dO-^6PqEo)D83o!QTT$G+y(139re-ms zM|F$3+6fplQvpxiz#*Zv&0zkk0x=6@-HH?G}773madnZ)D(A%ntcoD;n-1Ie9!6Vagso}dvF)!h*n;!3HG z`B%HpLyaZ6q*EsQ>kQ8vfE;QX5~b2ruv*mZktt)j(Tx|M4(Bc_*f{MkfTELAzUD z;t{^~^rIovil!m?DuJ@_C!;SWFIM0bdCiHkRuc3R2Po=?+!z`rBQ761lFdWdYY1Ig z4f6PRKeXY`d2ltx^GFpsNQgc_q0}Hx3j>F)A(a>u#S#626$8sT$R#0_9-6?g{r^6d$)4zrpGUE$ANtCJ8@OcdY4@ zT$>pOBp}-P&aEUUhd%OA*6XjoyQ)i%_$CxAB+!mC zg$DUi)H~24?aZqaOUXm2O=v->9^Kd++V4Y|*in6r=B=6XVoKzNolSOtFxijmbyW>t zoF|s;WB#!BBUI=RHaE+jKUe_fc37we3+@Z{uWg7PWN?tXB#q>lh_*<$n) zJ>ZDzz5j=_w+ySR+p>g%1$TFMcZURbcRvu^-TiQIcMtCF?(XjH0RjYvK$`d7`*l@U zcfDQJ-;ee4?C05M&o$SWV~sgxUlk5KGNlI?G_iz3Rxmc&=5!w+ zq`Q*?b{{N~0@|bli>yP0?uBJg2k#M45IcbnPoJa%i=;zJus7_qBa7m7JV6$<*6uEN zpD%$PMQd&_HmX2xUoa%HuXk>J~ z{qJ9Er@tnCAL{$uM^4ZP%SYe>%f;I&=MWP7@wQIaIDI2d*N%|H$cm?+R5Twl=v=Yu zF#;dBz7i*21Bq)VB!jQlmIOY0edZ3=GC?zkyA3cxnZYxpPkKlO;@6%8K0{PC7r{;__KZfeb6HLW;f*9yM+%rz5l*khgH-TQUe@EY?A3H`g0 zR%bS*KM>QOgJF9t!ag*X-T65@@w%*5Xkhy2WNo?!1#6}@+dmxBpO+!WWXRBT$PhmA zv2NeKcHbT}6smR8qJ87uZyb&3AB*WP%urpwPu$Gb;@jls0`BJ|P*~GYaWC6^YZ$`s zzB=uHyK}TgNB_=9|E{m)Lreb-LH}-|<%3KA&QAX>tK~yU|Bg@p?ycp6N&ikw-%ZZY zoq=935Qa&~(4B($Sidi&&bqmIGP3`2zHkw$9QJCY9>xi_85?xA;+TiBt@K!mC$62G^N%jT{l z1ur4<(p@$hmq;XPa-Gm~95XXe$F5R`FtULFmCm}{kjmynL_~i>67zgxP)=1wR=f+w z8p)sF^oaX&qXfOJBo6^L#3@JEwWZ|&w{mL~9rLhGNfa{@A@f1_blievmjEZlnkKKZ zz%4oeZlM1QXkQwSAnXZO_&UUnjqmVXD7I)@@vAK17%)%xDYq>iDw({9ihM|2W-V<( zX{lF#c2;``o@PZ_Q-$0TtGw3{(||6(9G4%7BT;% zB#^Qm)%1M1wQM;{>3r*g9aj*!SzIGj(p9`f))|2(IJ5AR@e;nV`hl-Sqbj!a5l@!d z;$an5rz;bqi+di^s~OQTK0#f%1X7-14;;_s+;E&C@0`qtOg|~z0+De1dx0$QNZtka z{Fns)xb%etsVc+p)c4EnR;9&U0DZ9(Be`5;oMkJY$uj-CUZFD@P^zO_&fi&Ux~F6nZf%&`-UGk-~V|eivGVx;y-QU{uzp{ zaXoPTEGVJ=W&Y)^H()-W2st7%VVK~8#A`EkZIHvc!a2%=D-lo;?}-{3({s zu#VlfS@UjTeHvHNc7z49>jy>txGr4V@H~GiO;KEpgRyE)LC$e&PTDHhGAsmT<8iX0 z=t)8EMHc>w$u4@)iJq80SjShea}|7dzKAHk|Dd>g4eFZPKmG{TzooeUz8m!47e@al z-lPNZgZqct$(WHAi<^b2{H6>+3x>&XGQ_vhVxEl%R)nhbNu2Zxh1`cr)2*xjr`0}Q z4B$Kk-T5r28gKC_Mb4yFhFzAJhYH?gd^6I%Xj#72*(nVzC0Z|pzb*s0MB+_Ki(rA$ zdn;4{w&IV;YxK$|KoBu-Z3ODr{;#}rN~Edn`|*mgAL~Dl2Fd@w(I9Q>@GsNUA~jt_ z^sgB2pcJOXBRcRQd>SiVCi<|I{CJW7EJ|vR8 z)X0jA1fRn;N+;%kZm1OHzA`(*3*wAO0^rOigD>_8_Kz!J7&fG~6sy1`9iJ$j4Q)Zu zqfaV7k!H1OIC$A6xM=aFTKt{3I>>1R-CD+yEmbFez^AO(cVub(9(VxK;r;#8po$uJ zn6rs~Zz~g5>gC?Hf`QOLxWy&J>0&XdT9NR}XE<*3xEx{3RUg}ug2mY6Q!s)kW!a$H z!VYR5y+TpKiGnWQ2Fg zx?EbsIGK%-sTirIya}2r78^pv7Ry>|6g>h&b(n#Ka={x~>8v>VUPknJni-D0uYHGY zILjf^k_$s7|EosS2_il>>jaC_;d32r|EX)+D9fgs3YilOZJOCO*HovdJ(;e+gva{k zrcfv^{BumpA9rk<$;=H(L`h*BEL*Kp6OUzm`*6pZ8mMf~_ksz8H?=M|#T?b8c9{u` z%kF4s4=Hld!OxorjrPOgAD=<@feC`GkiH#vaj=-jBrQ?T?H{mLQ+Q{T^uQr)1S!p@PcbFL(T~Fv&jESB z7}WADktBU==E@{!SzU)6x-~g>L-K(iD@GdHWWW)@B*@_To1B2}=_VOh1{5_?N}oPM zJoSK6RuOFT=6EynA4GWJ+FwUu{Fn!-)CrgTG7ri&_fH?31vJbBG2~9_M`m>RSKGz^ znFpc#zwQMWQyV+ezvEf|@5lVV9`SE1h}u7%sbcErYU=Fr&)nLizVp#Zj`7X}qyR#( zl3)yCsmZ}uU{o~){T|LpK^ZTWYJ^I4K>VO2f7+ou!@q2f2=R-WzbIM5^Gz?F#IV0Z z_+FaqysWzM2|k*klT|8=D6l7HIbR9xwCy~*-tJ$S&w%OmntkF#7yxPPI@;|l-BxgM z@9P%#!ME3qSG6`*j}Yc?x1#ra?T~KEOu@hXZ4keq@JR?%*Yb-cZ%ts#R(HbjGbL+N zs{nVd)D-nRE+{3Pe;tH1V=cjy!$OdAu#6%qg)^lXTT!MB`mQxuJioxC)s@nolVc#q z&Y@VGFm|(6M(4jr=U;AeCK0pAsgMD>v;Z}HKZ+CxqjGAuJ1Z{YN5;t*H;}>p?3mVP z?>NPU*k0#D0|PlXzRNBbs=1|Kj4sCC*0eJDYT11j$CovA4*D6zvMBM21;b-m{R^f2 z+V0Z`4iYhpUZ3}=8H^1~O4$a2?VMPyq~DJ|2feR^zeV`VNR;LkXkk(sOM|?gaFM1? zKj$)HX_R8D%j87g& zgEpEG5@c4%=7a7V%-r1|Zz@^Dp@(AWkqdbCr`&2G ziFgVODpuhYR&&5X6v0=S)tRrRv5#$`^|{Ux`-`D*JDXpiU%gi?9d?fhCl8| zt#3dO-d&Aaf*BR5VNG(|HZHexB336$zW4&y%$5YkmY!Td@!iVQEPFRI z*&vX{jvn1Q)Pa%6<_uPakkOmFtg3^2#SRZ$oGwX6&k=$#9aIa{1!Gu%XxNDammh8~ zUb8ML`39>ERe^#o7hH67>C0NI@$7=2joy5}*Eq>!Cp%gpA+e^s=PdaYO!6|#eaKJFqI-wf$aF=FX0v-d5N z*-pp3G(odpmf_AgEeh{H9C6sI2Gq~7bcoAzTIO1$U&>IO8BHtbH;Qb&zixkpCgrnC zrl1uX`H{1KihOz_>qtf-Zm$$E`iQnI*<&ka&QNW|%p#`8x)YjXnTJ`a7_lcU1t!O@lP=jWuJNC_7)_sA}gIlDd=cSuab zd_MfG80V!MeMefM{?G&7EiU5}F6iZ<9z@hJ)HJ7wcigM=_SwwO6`SkwK9Z(v?W_`y zD^l`>^%_=W=|W}UF3ZyL6pb$Mf~raAAXvHfr~@|e?@SNm#YE~B&qrozDoQe+nV>)w zIlGaseGoaju?|xZK9<5ymDD_Yu-%=+K7HuF>!>|Egr5?iS3?z94ubrPlBHhEqQd9d zl(nhgWQ7On-bYT?#GS5bg^F@0g5OVK9fRD6$WYNsM2I(s%puVwyMCQU34Anw>0?Qv zdM=X1k|mR`@iiq}ea|YQA9Mm{xF#LjM@0w)s;cEWr{x|QNc)x>kt zGd6{Pur4m#rh^xXYyFv2GQ9dJx$t2R0u~m%=bcz4SP61r&XLf%NSbrja53 zxyuCjRSyftwvBKZu`ikNCJdle#pha#VzwzyZ+9rjxCzZ%k|Wv7Hw)V1x~E-!d?}GG zwoD(sz(TxnQYRLtlW!%hy85Sz%&#QTboxVSl7jGeU`*n_dI0^$(?bN{Y%2fJ2KYY< z#ca)Ick~s^_hGaY21Nv#<}y-%p^dae2nxCliGj2j!$jZkpaMAVM6e*q6prQLmkeN` z{Nt>Py{N8?E}ZR|u~eM={A^=Uq5KN*Yt3C&&iqvu?pQq|4*}12&#Uf^ovZD~rziib ztxvTOe}3hOeC;SJx-ZQUv@0C0Z^!i=W4mtz#GIvRIT*VzA zuvfkQl4@#ZJJlH4sHX5(AQSkjK4?AYcW>WxKAf;x4u}@x-0xUQU=AB!jv&mH-lJB` zd9v}|QBZX0KB%I=nkD6wpG*yLcwaC)IGA!8Q~e25O)0rqZlIa1juna%3QKZ3mJcdf zXP%2eH87)b-g1iIe1**|WjU7-dv$u7|3o8qermq)A)Q9(M_w$PP+gyF@VTJ^K< zdPVQTDo0`OhH3N}Smm$|@iI%CIG{o+^j?jXvOb+>oPuLGWBgkQ92{4<$0ttXChZ}W zMH;Pj>ohMzCc$wT>jGN+#Yq{fGEzp9urArk%}YM@l~$f##d)6u!b&ar>GHfQB0eud zUQ63N0y2XzRmjZo%Pc_ESy2zx%~j@dn6XBoL6p=^Df0N46J-W+aNnU~pKG0oV6xdU zJ7#4<*2a?h^z3L$YUPfAGac%SCgK4VU35f}{IWu2ruAN_s_yYiYzg&W9)Mq^T(*mz z9+$Pip~r&S$fF-&i-#0a`a~Au!aQ>$Na%)sNMpJ_L7EV)Tt4Z^E^?)e3A2h;^Bx4% zMI<4$sJkhj`n~ol3PLnf%_?!vxRsa|xlGxhLwV_lGw-Y8^nN8?-H6>z<-`;v+m8@4 z{4`@U7`o>qHKVpV|-)m4%*h>QcApz_1umY#9$zfPqQ++wFHEm*Va3?1_h? zw*aULpG6*CmNLrwO5#L<32SkBr}uB9pEc*%Y^PY(;3{O~-Q+FO!QR9axD&l}7s7~D zlk}J=6w7%+QZXPsaAyZe2Xb12JCgRH_OyFw=?@Lm9r0=$sR*0cET^!-bQx6FE2j}% zOSFGkV&B7(fq-XOkdy;Vf$Z$ZiG*O#9LG^u-0i>mz#{cr73SbWltM`*1I=Kn4 z@`2d-wU^0o&;A%Kj#87r$b6I*H6Lw^d^&4$xL_|@em9w!)nE-U=Ub-9eSPJhaM#yOUlE&| zzJG^%uilqeZte|8o}jq*1$hWnc1<+3sFyxg7nFw1EPm23hNWw}b<_kCehf_T<$Uz% zcj!_PFnaQZnUI-Ib^c>C)}8Xlqi%weng97neWXTHK;=oF%C<6pP;~S1Q1iF;kMyB| zE2CgdKQ8_FAl)Zj@~z4|FMv>(geTb|>rQn7g(sw~yv66eI48uZJcn7~NDHD^15sU? zJEnEBT5|<)tG7m77*&o!iPGNG>un%im>(sS(^dK06=Y{MukKXK_Ll z%)90kK(#t{n_iSj2R2ogxkdp5(VGKl*J!3f2*kym5!v)3^ui%EM%9BSgM)A$I+(*=T`_!j4uC{>#rogU!SS5+4@R0tw`jpo=hY<8bEc2#3ftQ;7 zUhyaGU!n^{H=^w&g{QG7;rv<;DQMYE)zc>wHT6k9p1fR=4N0V7nnP=hRf%Toku~^9 z!a}9iNNDn9P-h;rEf~}fL66`mA!G(WUBMN>sgVz4=Hi#omc3zaM_}XK=K1F2zBJ~C zCi%|SHN{LBLv5HsT)Pf@D{L)~n|{grja?;Fm@b0X{MH1fF)AXi}Km^tDgyxCzCzW>O+G`Za#V!P>Xm={yQSHOl?=m z=?@|wh2=|L?fr=8dxM(RNvY@j%JbCLlgyA%1J)Ue%PH^(jO~^BSkTT$`W|lQ(nN;e zJzw{gjgW#9G4n^Cj|PJyj8J3TpyAi5tQiUY6Zj^IC341G zB;?1V_#OtdEKGrul6QUT2xs0GtH`h--d2Iwb92;A;QO&rmEMY;NQ=d=fKVEiZ}Lbq zPtQ~fkKq%k%M2iU)cG4$Pi7x+(9&dzvr{5H6+KZp*6!Pa-aNr|@eK~cGD|S^*))qy zXBcfaq+&|IqYEl(iD|@>Fow;5B%3ywII7w<7Q5W=Hh6l}533zpKmCkb?5EhQB>!PU zy?{s2__PWne<0;J2VrMh?UZ40FFnV-4y;&3B$E1p$uHUv5O()l(!ywzrsHl@?zA#13&+F1;PJbIy+hZW%T*)v^+&>w$6C!7=F!xA-zt7h#^58%DRRIpSUa zYe}91>JdkQ$9f9Xr~Od`?-{|ztXqMZHuJ}o9{(4pUP1x5-7HY0kzmg8bl{pTzh0qm z1LReD{?sLhw^_0NHs+?hFsZ$`C-H_^$-w14DYo4wAw=YNYp|nLGo%`a5ddLz9}VXA zABUEgS{?5iFWCXTXIlPmXH4kXF4C=8yY+beY^AFo2{IK9Z~DYKd0eB(fr`O9;7t7Ud!m0^mzVL&;XTzl2DZPug45tddFQTvbgg=dL=SMz%J) zJE3b!)GuHRrnl$-h$;z$(#uB+MXP#<2WjOeWs#WU;b9>@<;hyFefh*UN1GX>`PwD; z18w8nscg)u&MtVZc+*=!@Fc!H4JS3r2FMbkiO?BEFSfeHO^Aqp^v7{xap=p$kNdRm zDU&+;s11ZU#C*o#$W)1GlJmWE%car9D}GDE32MK~!5|jqW$?p{GR%5|zyDxj&B;_! z9|)psex=CU-noTAh~cP|vl?op2O|9ZeEk`5_B1`vJK{pf`2l(*t1t6&{iyQD^N8=V zs|26HhWEIx9oI8(xbkQH^4Ac-+iIHmz3LH%iRQHumqgfDg&oe0v--2qZiSVp7$du# zW0gLOFLqgf%(%h?j_w&UK20z+1=FAjE<4G~0{h`Wth38*+<;qa*9dt(Iut1(=k?lHA-alY7GcKpl6B>S<2QbJOjy37TWtB=sJT z#_UnLg3A=N4AR$26P+z+uJ{cuEo{d9VnW}Wpkw9=>*v(d{LW`bQ!Z4wTdZlFdi_n! z$MsToFgCvR4MJt!0B`lYV6i&B`@B_*Qh{`z8pW{_qzkvtW$;KMRDzSPt!1qd<9H&A zo0_QJ^I~G6>2~*6L98^%j8nwLRKUR$n8TL|{KKpVOM;H{`d;Wm-1Lhv!v8ym9c?rJVX}K8!4q!! zv6@TA_MJHVeX%fJHTV6g%^Q?kjDlBw#cGrY*Jn?ndJr3(0rY(1IC~EoYl6=ox%xj8 zZRPn^|LnrYz&h3YYhDS&|Kia0AEk_cZ;tX8DgUoL@n7KN8U80H|JrH32K}FK@`b9) zb%=F%OCOwkhfy*TgYRzT9EUS=cd`z5-}D$jMvKb{Gsc*cSRwjF?t_vyy>3TV{y$Rk z{$7o0J|8t+%*XnRlK&?S>G>brUr7J93N*2|{ZLZqT0+P#jxzLu+&4hSYI` ze_RZ7n8bdAf6~onF?hl=xcs_v&w_LJt#3<9kDot3RBf0kt}nuj?Z7^sy&ng@x4fNo zENT6LM0P4aw19OBBQ6*S|8+Poj}xJzI-DRlf!X#jf?wAf zUmJw|Oz%{J`#C%;jmvF%6{D;3q5T5FwWC#=q3ijzT3C0JNltV5*M@fX-cg4&0+P8E z;sTd0J%S3EHUOrQ-&-K+J2(h{sl53O#`JfB7MXuHl-C63r59gnrC8}hN(6}rU=(~) zgA^VG%ScaLDR(b!vyHNlPDYzGsOU2OY>7?t&6i3Wv(E z3fV02jsP#3t3;eCzO&sE3bybY1ulpw?FWbMrV48$FDPuUU83Zezy(JIyDHV~(X%Rf z`d;d2kZL@qcitN$5=$s+AX*QkrXw;51e}rGdJR}`Ep$n|d4F5jH{!Kz88%Eet{Cw~ z&cN0`G~_;Sp0nyuYx4}Jbv8w(KU;!&c}IIo?ptDYwE7fLPdwlk6~=>s9SWp_NYZ5e z;>!)=OT|$A%e~ghMTu{C7F9w31>VDG5yIpv@}M9K>aX6y-@$QLMn@LDc59N_z3}^p!zW?K94=lAvJzz_0!u&n zCH|-~m4mnbc1=q3d^y1((m2wdR&FO#wqPYu${=+>otFOU6ds)|9X6|-_Q`F8=Y zuIrAjj`6NwmsqH(qe9b|6xp;&0YwH=+PIj4!jTZkMaeU?!666=9Ft3@2nmYqemX5s zWXkmzs?}Jhl)YQ?-_J#P6e5J$_=Ie$(J<38v;FGx>ZI`gF}t+n)AYVBB3PjIh|Oq} z&35xea5N(B1S zbAOYbxY7HEdtB`FqD>^18C1}iTC&PSJ3U*30|B@w-vdB6TlgRoff{~BR@u@sMKyQ2 zq@1;0@H=^w6b4?fG9g|tP4EC;K7uhq%vl&~7`un6WY$;W;1-uV(*BA7PEErg$5jYQ-ZGusF%y~71C#on~XZ(uL)(@-#9zv)mkc;o67v= z50Yk1J)lcyh%~ukoINxLJOleKnfSH`Ue(gqV&d3-OZ1PvKyIjS}7&-Ds}NeCX7)=Pq0r z->Mi>7otLf$aNNlhU|~vn>mjO05_KZ-ZRf;-4c0Ro&kt1(Qn95UuZ7nPpmncrd(+% zI{WZtb4BDZ_I9`Vg|U4I-?6x%x|olcl%FOv?sp(g4 zo5zr?H>c;&zUH^z$puak+2J-r<@{z9m+=Gr%>nEk*~4pRZ3~M*ByS3q&g8!xz$rKl zC46yjO*d108K5PfUB4;TZv=xyuuGj%1AG0EbH3n&cl9As5SeHN29rWbVDMmQ7|8{a zqe8{dU4x2yTfxzlp<<9F{K#=Gj9QI>d^lw|dq~4{5{1D9n4;%KoyKDPJO-XoV}_?s zpH%J$sbOU*#Au6?Q~sQ_3{rb%+|fPXEv|kp&f%9B{nVXUy^N>eu-sVuIO5K%T;0ii z1Bpw%HFRc#ZO-iofM|#k(VujWKi@B4f{Se)is?r2?vEur$lM)y@sV9$ISY(??n z!|!}TftD08F^5ytXHfZVElehGf7%g%!~grXp(m%8h0EcVwn;?*(fkb*Nj zmOw*&bQj*Itn%e1Isc>g*cV2KTwJRFS_TG@s+RjpV~X;v<=bxTgoOh-eaIbEcJv?mwfea==S~fo*GB0wTr?YOeuO`#q?q4@B12k7>Q2|=6ZRB!uZxojY)eciL zIrIc)MDRN{-Z2Ml#gq_^v%;91Ehnd8Y;rl*>~`y$mucf8C_9h812z!MOcJ;`T$y&hg=ut|#D8Ic0$D<^OR2W~ zVxdU1)_XLD>UQBT^6E}6T z0?~w5p{tgz1tC#D<49Kl`W%wXy{?|CtBx0q^%aEZeM-StETh^$M4*5Mu0iZCb=&RB z6g&FstEK$FVuqTNmLdMK7+15T5B^W3Cgq}4;PkoCur1z@NcGyxvO>yv>GV&S-BcO; zr?{n1uDlBu4Mcr$l$?OCH|{XJz$MjZq-7aJE9^AB5gVAF;-Yb|eZg8V$NR!CmJpLV zRi~eDD=_hmrujyGYm$ZUlKL)?nkJo^jH;YbqCL=TsxxmLet?nZ_SlnGHe&y#xsfK&r8Q_ z?^OqmT^EG*V|1(+&k=QfWpnb+*SL6i>9SRs21Z!CjI(gdO;<;(rtMa6K`jk!#%(Uw z?Um{{1{j~|vdzLgT`dmjkGg`ZE;7g84PpTG=A^pgST2k;ta5ji7Z;|HNi13Pm)Iv2 zYyEyPK?dFUX(}wL4RRYcwk%TN-;(=UmU#BRJ`EV*Xrc@;g1!vHhf~6>m=`n=EC?x_ zBrc&0ZENgGlE7#O@WXur3j&v;hk-*u+pP{l0XKG2{E9PxU;?#pT;%o^_aU8_-A9-c zDKOJgA7F)Y%LYi{P&@qvZlAJnH8K|t)ya5E?^w{J#-!N9?4&-xZD+L+sw@KOfMb+G zmE$@Xq43^_5<8@kGr!7@=@uNIK{wX5Be~rx%4INKsjPhLh7;5{~@7p zWz9S5W}@QU+7Z`VfY6_`I!Sw#aSe6IeP`XLaw8(er{;nmbWHXTb|4@8uDQpmL)xQ6 zQG#X8bbZ|}6#%|VN&8c8xHC=yoOrJ;wOc_q>`n{Y`K3CkeV#qYR5_!2uzx-^$LfMP z1m(W3UXxs$s17ftevxu9*XYj-en0!;pDak&j)#AYt=>|t{~h%a^j!Z7w*8Mq;Ftd% zw*99U!e7H`TmW`1&i{)1B;CIn+X7~stgAL5zkUV#`t^$`<`tBqafS$mEuZ{4v>{)f zw(NKLGzvB7VdA4icA+PT-^wx6sm<_J0#6Bt8y~4U@#5$)H@D~0E>x%D7vjK1TMiu4 z?K*XCL9+$+5i`UIu5{T7g@V1+JM@^te$?nT`QwO~%V*G~H!Kbbi{Cd6L>=x6Jayz4 zttD!f?D%>S={b6+rc!trae7}Gm=~fz*dp}G=W5fHW|*i(AJYQA@dStHFT#S>C}}p9 zz@SYh3P9Qya6{FD!Tvap)_m!qdNs90ecoIkD>yA4$8Xnz!)su#9z7k(Wm$$npO(DF zZE|cz#0@%R@oC<{bK~%DHfOB_OwyB%F))|-Sl1lN~=xinq`pRRw(89W3+e;!X81ZQDlU5Lo z1mPM4gx6PNHMKqn1S&_|q+GQc)Z`pIMQFr{@zO^+Zj%MBxe*D@h%44VGrBQ6I*Iv0 z6Q=6i6Qiu2dE{?%?p!MhTBTb~et{5a8S*M1?VzPqB z#XxYe!N!tm1f%Oft5)L7O?Ed~P}LiFN`5e}IG!sW>YjCGEvyh+H=S|b9*~>3dO)z% zed^+G_0?P%hx{xy@{RU1lzYHqwUdISSc_{SmkTMa0w3BHD+^j-sWcUF1&)V^;2N1R zwzv7$A87O(I@&4Tp{=AQ@T1?tlMlLQ#)0lAJHZUu){zI2XW)VNs4T&bq`|QVlxOk*=blbgFUsyGwdq%>AH+%$8y;wE}*}WYsRww@udbulMsb76;F7^M# zU-v(5uD_?6|F;w6U*-k>VRse9cYF*IL=nGApzb3@-p0&>fZLpfijyM{qevuKEBqeL z^+2~~bS3kU5%iBKxPOVT*TKGuinBL7o=#hsJ-i*O!r`$dQX&f=pixay z(`pTh_ZlyxDpQ1b){Mz3=%rBTMdV73_5X5QSeQ2c-GG$Uz`C)mv>t(9J0&_xD zfr%(O`^uNIC#b2D&H3fJtO0N?7mk$AHpa5%L6@6Nx3GU90TH}m9O(}&F8If{`_ILy z|4k(D@xjb2&Ht^=&t7%+$r?(BzBX^{NT7_psIwXC(i{{2DevUj^eUrct=X~dt~q7O zI8o1_JCyPb4ZSz%%V}50c7>(rCzIYJC3$L!yHBK~aTEGKTj5DlJ2nMp*b9tV{sag= zwd|O0^Re6C#`_cTA$*D)I?;|JHIkMz@pg7}eym<@w%wW{92#+V<7jDdv$6h3XZOR# z{_29pu2&|BzHd6_vWn4eWcz{P8Xl*<;ih$Xzj-(&#kd?6XB|b}TYc|`%P6C}N_=5} zX*_Y`GukGP9sQ~2TV{T=WY&*}0{S`+w(MVkKcEn< zT#F9Wiv_vU930tylv?rp^!4tOswC69Oc(vJVJN_0bQd5Z&Y%(a3d?u%t#%%jWxJVB zU(tK7Q1@uRPVRS(n-Glw*$k8NPY-Ep(-{h+!TX1cINuDT-i_~?_yop zP^(KF&~vmGE}8{Wq)PTlsn3i(aZ(aj)s?x^OBGE}CCP}b&A>V&1W+pm7RK_{ba3UJ z%@@Wf29$r~S_lwUC@cgNqE)YbW69|^kZ)fy;L)&soE|`n*zx zWfyTqlAqBSAnv(nmv?5paLf?3rN$iS?QV1W1`uMph+0zbQ#b_oY4sC{Y165gO6K{) zSZ>%w>qZBogzw$O42x8!U-Jl;(Gv>fcrjn3GhNH+R^eBd$~|0Cgq>5DGWOV87Y~T+ zMX+Rp-M4pTEg-oeJ&5;9gG`E7Zt7)r)xwiX^m7~*S@RtFZN|>suTbySBa1wEJ{KGy z84MxL1xD~WHZVj|<(MCH;WIx3-dS0mb-QK*31@U#5Jp&W9WArQ2V9Q{$|9(v64N9W zBE>h~y#%lnZzlJ<&uVVxkV;S1+6k$xI1j{zj2Q6TYg6+*Os`!z=eg3h?~OeiQ$x3; zPSY6c#KO#^`g*b0<@Y=5w)G|Zd72WF%Pi7Yda~b*+qnqxJ1$GFn$>A97kxUE`}+)BQfp-E*@ZRg%!y*aj8-!#=zyL6Q^k z+@3A;a_t19Phc;y3Ei5*%aV0_1~UzkTCaFSF++b-|FLV&99}RXdrYie#BY~=(Drt} z4d-T`#ALjt>pr1^`G#-0Pav%}OU`>$N@w5xCr$3=)TEwUf*Unvts@SMUy zrs%^%%gzCPXF=&FMbE|;_RF-)e2|G<#EIFT;$NO{yT06u-tttQ7M~YI0VVlYZW4l+ zGJ6Z7o8@P?TIq7b3r~lIBXOPqj+93T7C-T|%V%%71BZAF-kU{gz|E*i5U-wS z*AIRn3O~8pDp{)PT1#5bSb=qGhj4=b?whuq3MPqz09QzIK)b+>)3(B1={qlq1D6k0&gh}Q9h$HC9$NDz@|#Y z0rmQi(41gJVT4mzfGkN4(ZD=N@m7N%G6eEX3IGmitV0D*px>dN5DkPbf$}qjB`8j@ zRZ~PLFW@==2;MK%&rA_T5k)3I2EYJt?8O1+fUH1jiYU@3N&!j$1At&J6>S)G7(FMI zC5I*OIspg^3^aL9B|B<5kIHrH+0-u3UF!7`SFaRkj34j!U3Wx)W1BZZpXjteeC?F{<5iJ=U zDgZ427XSpB0*`?4XesC#D8o`(U$vxhm;sb|Ko)5c*t{ebbrD(sCIAFf2QC3cU@Ks) zVVo%{Nh>KaWVK`+VBjfvfNcF}u%scd3gA5u zGEZA1XcyH@>KaD6p9xl4Bx077RQ&sE;3|0@NJ;iIUP-o;{I`?{VP1|yC(ug-0_=(# z5r_y(q_iQip~#{FkOR>75MVQ5GGWc7SLLM8r<3ETY)JJ;0PuP74s}3@evN)pv}Uvn z7)==qG8=L|MgV?Zx!2c|{Jg7ku9lL-Kwm+X)V)aX~}7esSIb3^@^ zS{pel;h`jgpO<}I3v?5K-cyDp1S<3&pz*I3ciBhlJn z1%auQcPI~N28p%UY-cO@XfeUWd%=|R>vMWtExy*yn)Qy>y6i%I6%AJdk(#KMN>4xIS{U$le4#DSdG> z(bPQXE}q_cg~UMgIUQ{NX=C?;7M8dGqj*FUi`c-XBcR+wpaRbS6f9WLhipi4UlmOD zvmN*9^6HkpQzaqcn?1u*;nWS^qZ=n@YKsbYtkAOE@f@GWPXlXNNt2mvCOxL#RhLQc zz35u{^=@F>LB5}A%`YvyR3MDZ)(F0_ZGzV>1B%AAT^cQAT^|$JGV}ua^2cu ztrA!|Cr!<9jjR$_J7-PZa8a*jSvV(5jd7K%W?4CBOr3F=uVz^~r%Z`tigTH7WSKZu zPkC}h+D^B$^&X34M%pknw+$YvWKP&HwY2pg%Vbh=$=X)ex2+%JWm?Wmx3#Np zTQgff#>$N5YFW3bZ<{h}n7ZWBwfR}QX60NmrO<)4Xv}yJqR!G9}2>VdJ)OQP-BL z5}lXHY!L3(`O^>K^}1bv$jAW^@6W_fF9b2)ZAG8hURdP*u_)EIs0Ex7XdH8v>B(vx>)yW?*9w{Y<`vg>e|w9}7=B?t-&u57Hq`o)XI9&7 z{Fa(A_CkMdZ-sS^zwBQjvMj9iry@e@4+Y}L$fZp_3P^}E^o)B_%{g5gv}Q%bKB81^ zY*xMZQU8@8+%AG2I}G?V#c3Py$S@kma1m5sv5(+>G_abp=4;ie0M%@I1J;Gdr` ziH@MCLVN5tpsI#=t2U5Zu5)|_5>|DcWJjFQxSw=Jm{CQXyhUVI!%xG;HOS{;W9IDC zvo$sVJUKU|dnWI3(oJ{VIO-*OG`tD)a240J4U9fJ*ctFzw< z4L~ot6?6R#u>uHvdkS0c6R_}*YGNa1_Yt}LM#SYKb7?GO`y{(G?6Z9Bxisz5cTKs} zCt&;(@#9-}xtnkxR4yp7%c}>p(D@PoV()yBLp1li*T$Q^{{d~;+FPNi)s6#UoeD6z zk%_WF)HFG?Xhkxq1>v(6f^=D&I5%Th#Rl|oL^(Hu^EftRS>*~541KK1A^vFtO^#o&* z08(V3vVKA^i2%*87F#``nPh;3P2^Utv7>PDy8F}0_G)k%IDCSYOZSLz#PQz4S#=La zT)Hv}LWoVOv9xh+8%O(SmdUV9y0PF)0>ZBs)a3Y&JV*lz#>5gTk?1TcIu|3QbNgyhI*bY(jv#2VaP0(E7P&Up+BTv9w<&ZDVM$;?~ zqmBTuPBbzYr%FcvRHqs_fYZ_Ffma|OY0kRd*14{K`6m45_aUu*a~UFn@8%q5Gwg{xT{~gA6eyn#3w%k*o~%%twkCa!X_OG799u z#EO6i#J3_Xhe6b85P(Z|!kF%JqhRzvYx+PP;eNuz6(z}HMOrq+_)1~It7e`gDnJf< z&YnRMKL=$nOdB`eieklH9XcH4okx2BYccHsrNbr^upA}aR~^+O+F_64^73)8?jZ1N zw?HafhwO^wLQ_9C_xtS-@vOPCW$kx-Qn=TLE_wbLxZoQ*fZLtAC%5zJ(0nD^y84Ky zw?7jk`v&FE=Fg{hW{9Bq9PH2}47T!uzvuPGuC=d+*zPVDnftXSME4DPm&>QGb;}>} z=XDRX`*TkK#CuO4d7vB*&S*I#9SDKd@$RYVuQOGB3E$WELE;DHAZ3o$!f+9Vyqe_+vaD zBcA&EABt7`?Yf)S$&sC2ZSjT?K{CH3?&~KVrfd6#gu@-}eOoW>P@5ZTg3k>vDp`1q zUV&IOE1J&{NGqxHT^v8~y_GuIPu}fqH^yWaA6ZQr>dMk>As`B zt5fuCT7{F=L$;eC?3!rTm|H;^Ag1j+=nhm{;u z6!7Q83cMA?2}{9dh#a*9R%zhyJbHGrEhuP+#d#p+cmR~())@(DLGL3z(fmTl88LUP zg~znndm2Y^rHKe<&_a*kpr|O62_S79PUZNku^pP{*TkUMq+{#~Y%BKUp@a#Ro2fsg z))@HR*dAjT!70p(r9W+284wB)wGtpnP5W5B?el=b4eHRdW{4>?gih%ey=Y$x!#UVK zyFe5y{wTLme^JbwE!ttZ1_RWTE*}LW%j1lW`eVxx1dcg0?te_s+0sFqBkw+}7y}~y zT=6)R*5z%OjZ2k-M;O?G!Ls^1j6)JXBEMe0Xi~pF#1TfZ8J=%;8)B!rl1a8U*Rc-O z;b4J!=u?y48iUUf%=xngYL508J~UOG%+IX9k6K&qdui}0@`bt0K9|%e*o}VXt7983 zFh1>wRsO-F<)M3-RldK|U+KYj?(ki1(Wa%;@9Y(%m5fl|8az`3^G4BR6&hH@de;VP zFHrVNoiZ7TJ@WNQMbeYG<(>zX5#70z6Yj)qZycOWA&Vs@lX;W8Io5)yy zN&iWMC=3c+UBIZd8{cU8#8F^PeyGbm^K+`}fZHO9yP)T(M2F(<`bo0x>Jzfd>9L~!S4>!HMAWk;TdH!V5J)u@% zsWcXsO5vH8p7PB7))Gt>_A+&W5uoUaYP_i1q+(i6d4pzJJ7~NZ8K?X4YZ-Z-t`3Bm z5i(J({H)kqj><-Ojnpm5p9oR!KT=e3MZ!M5R%0}o9e^aqT)k^W{Y)K^a_iW9ZRtza zBv`cdj!7-4(pHgNchYJpJW`b#Pvre#f;^G^el%5)+N!!UIBA%ae`#zt_lPq+?}aMb zk;b2vJRjGCY)B(RSr7W&MJlONfpqf4Jz=4<)- zDb`Oyi-fLBZ|3aBma4VGc*O>+h8C7T+^ha4_q+5>Lcod#$v^)XLko*?3-{L;T8_UJ zIg0%Eu7Ca|ZThD_!@o-f>eiZgAE`i-h<*2IbTDn$J?Udou25n#8o9b$X+Vmys@#SP z5EeHi-6gSbI=!M0ET*67z_RfY3~?aiMDNxR&3jLDzEO zOr1J5iwnv{l>L%(UiiKj4pFr#N8Mrjvvwqj(^H$!2z2zrFOL@Ck}!^VCiS?WD*m$4 z7%Ng?y|Xx3=LR(+Yl>a-jL92qC-hHakI!GgnWIF~G@)#Yhq)kS? z6F)Hmm1V1y98kT8Bj_XBgM7?ZqW9BnF5L$DW^hfmKCyG%b~<}iNZ1C@>LKR~=Zz5; zfnXhCAVo&is(4^w07=|QI+n6Mn10Bsgn#GBC!)3wQ#5%e4+4-0H=1$?iYdHCAy3hX zstUhvk->1-oU@o?^xfnp5dNKbqRI_x|$OBBvL_j2v2QGnX)Kqr6Et-HnmHdJJ>2U>?Kf6}Rr9k(W6lxE9U>f+^>Z~tB|M7NzR;1~Bhp8W)|Ytb z0~}Tbb?$|#m_iG}8ezfeZzX3o7IS2R59~lcV)^~Yjg8LbQsQrXdfOua13^kjhl@2B zuv@mP!ER7+#h;$7c%#gn@=CX{j+vxe_l`w{UrrDv)PG#gS}B;lPm2j092)S@l7PHq zJELhrs8G3AG|pd!YT_y&x}$nJuyPdC9IRT(QglCV{y6(fqf_S*#vlRM@TaLPD_K3C2=gPzn6vQ4MtU* z%x%6U`IC3Z$45}xNv&HRpz3Ab4nPoa^SUe^Pz1Q0qC^pJ5I8+{K^@@87!4aTDNy+}?8gB?XPcUx z`W`Zdu+C33;;60ZU&6Pg+43?h%}fBqHzgi-vh_cz3UbZ2pvvFDPhGazN@2VBG<@U% z_lod^bZ?P`z@;+$mg(91!##v<(4D?OE0hq-a!Pqc=zhsd>HZXx;xM8Yee6T5^M~}W znmz$=L3)K4*JbQhs0{p48Ca_bbJ0Zkexp60sAQgYIwWS7jyx4JsII^z zs(P32>WfSpx4ZUHV@N4?KSB}Na#-I4{pYYPya;VQ=tL!FM^_f=hXfqLD7FJ$wv6J zkpbCCjW!O!&w+9XwZM1c0}1wYHIyV9WC2bP;R8S(_#)T~igm%G^9bexIhli>Kq{_2 z#M5uAw5Q1;-Ghm!~@`oP#>(Y?Sn~)mL?O%HAA0GyfkxgIp zg75+chCq`3v`AUtW6*H>%(P*jpZ}S4`d_kz|H#FE4yF84Dps;kTvLJPM&l*M0*Od# zhJlwV{;?|4Ee&x36f6)Cvhfs)PseOlL=BF2ydtrYIYaNDCuV*BTb>a@YcIT5frX>X<_fD(fZ?}o!08oo^i9=k02&1l_U{gq z_be^DYjUlqIdh7T0^nmYhU2#E*i-hFxGBo`H$K#Rf;7p80%-z9A9F$zyg{ zhwtZ6EkphoAPBe-qbX#+(t+kkf4LHu;g1sKG4LGNyTeH*@3A6B5RR*|qD1JW>$MxM z7#FNuz?rD%&jEp*0hM?8Mr{sA{S#DOyK+g3nJ_(-Tcd7$-){RcEu8r>T}LIX^YjwL4@wV5iaKv^$1%=zPleHCLJtwnV%* zN56_Z^#j(3zq7>P6~56+$?+t%cTr${yuFgCX?f9RXMT@*LXv`X;V9Ndv=(Si7CcS2 zjqmM1RN+On)@W8Gxlgl23wQ}F*G9Y4r+!5P@Wmy)cdrf((1yE@nLpdXopskb)#I-k z%AkYCz__s47uM%J&*yoy5WEnZ0v( zocEJ~9i%7FjkuMLVJaPlM`K4W2QPUF3rETJ{HgLwz0MHhDDD?d%ihvSFJMGC)945& zuQael^99~}qVt^^f#5x_;5$aW4j;2DE>g@T&@8stPXGv5;uoKN`q2I>xR95sY3^?1 znT$d|npiQ$6hXh^NqvOm(0A;`*z-MA{2Nn5ImB~+q#Csd`vm!$md$*~W}zG0q*E85 z)Z}~Yt8eUd_kar)CR8R>n(#SG=gJ)zU7N+D4774DVCNf0B64U`T3qlCYaf3)OTDvd z4Ig~syTjk&yWdax|6dA(|AX&roa_z$O_5olEUo!j?s?mgj!5km(~+YH$xwsklI_x7(EY^5OLh_4MtyM(rAy_m<0ql| z?P%O1chSibly{kS>_7ub>b(Xtj-D@2(}gnU#i};*1dFejH4S4dQp$J-&3g#nPYo7c z)$v(qWr1&I>CCB@8l0XSTVeM|b2b}ldk6CU|qu{q@@Ht|aCGFg+DAdE`ovT8@ zb3g0Ap=PMsLsk;bs2|fSdgyFDS}p3DvC7WSX>`j7jCB24_DL!mkeqR(`sX27Y$Vgl zIh8QPm8ju&p#t{ZkLwM@q8aoEq6w;3smswofM5<$WG0#oBmzu3WzUEVZ!Zmp&b6lv}$;r!Nm%o%G=d8%qG zVk27{&sz057dGt)X)VfZDN%VXqgYjw`GmRdkbG&tqe5JCEnr^;vXsu9u%cbB#-iXAra<7%|2)8Jd0Tn{&g54!PSigol zs2h#6Qr9vZ|7vT2R0f~;YR2|RTZ>VTMo(Sz;DD!!Ijz=Mz;C$!+LK{I<9x1K+yeju7KU)~ z^$LCchi?f^H>%$xG76_yf(i(m0pzdGR>X@V7AmHxzo}Ggqp%7R8MYn*c&8C}xP;_; z_Y~Zt#mE~mp+vNxz<2jQp#qO=DKfdxb%aKbCilq>)laKEH~DLkhQwiK%zwCHI(#CbwP-it6UJFseh-nL5Sgn4 zcn&A<41!!`KKkr?K|0coRZ>*@66y73+`|Q+{@YQeb{PZ&Okm0TjMSfVrq`Var=Hl{ z`)4Fq@{T#t)Nf%6-Kgf>lzx3rh&l+L(@0%|to^CKoCtnKCrv<_X^t`s0MWrpU8> zXw|wKsF|z!LCA+EyL*T6V-nSpinHQVk}I#W5)#wC^-gvV4rhn2u@T4nDTG7a-i0Y* zV5rS0N^&WpV+b)ZdBKB%CLYD0#TOUq zi}vT>)ICu@$n0lK89%jDe7yIQ^49F&zPvP^MF@=#^m8&Q2zginEaE*+|0p3`ER$K#DteWCMJivT;L${ zz@-Y?`*+}3`6%JCw*BeEr9g?HlecYL&I+{6JdzuUhII-CePa80yLm@lHArKsLkmy1 zxv!>L9t(NDdwl@tB2Z!KL*LQ31Q5J(oBimtjTAh96m2cMhK>hYcNKkh&qpihdR#n8 znvT&;c{yrVJ_sdDWpbpW{ucXCD9js_uXDs4cQOmsIR*2hElD1h>qJ8mT2K>)H72T9 zOh?74X1A4c+gGV_icYbVVyHTy)>2Qc(;mkAl4Z0vemyC;cO9RSGI9OL+5cJUmHFbH zU+b5=tMtmK!LeFWCA2nO|6L%YpuTb{VIbQqM`ccOP7|?K&5Uhn@7iGiI&7}_dZq2D ztovLF_@<=3Huc*_EPRA@=yq=s%sx~yt7IQt-^4I>h<`VESej#VMgxeRU|5h{Pz_7qC|*Ws?HkR$t!f{zpbqjfc)pd80~Ahtv*<)43v1Jq z%)Q@tia=^ef}VNy9^vmfhzyg*&C%*=nm%9Ve(=+pQ9NHhj14AxJG(@)~U4kH!vA+ctpym< zAWjvok&=6F4~(Ze7;n1ZPL4NS>2v}%M{@#j`38#&5L0X{&ES&|MOWmg{Qw}b&rXkP zOSgf!J7RId3e&e}m1&`d*@jwwG5OUhZR#%xKnV!oz{;?A+)fxyi?SgWAfx4Iy+HONrU5+N2;!mJS4Xq1dtLFgsr zZ)I#ycAFE%fLNTq+59B>R+k6#h8OT-}+3Qh)@u$X77hZ^ZDM8EJBK@Q_3ZUvKIGubSb|qLd>GX3! zwA1)rP-`nI4h>&!`(Lt54+o{%8Pf*zoL~N&UhroJSNZw*xMY2nIREnl!~DNFV1LPU|9V}t zWX50t_~FZI)T)3WKDr0Kt*3MC3Sp~YuMb+UK^urTCkMznTm$e({Irib7IY~sYc0#H zdI9SG0)qzvV73rTTmWX3n^4})E-7k7Bw?7(K*Ab+Tx5VJ?0cxKmB2JQXy3A- zm;zC+KT0#2Zk7<0!I)p}vvgslHa`A52CSZIMLm7_aM*>mbs?8MIZE-PvXTt3800cN zfN3s>|A<$Vw~H348tV3zO$EYe|He~^r!GxaWxg3r?)#q$Ln-^IXSzQB|L5O6H@{yj z_~#S+uUAmT$ll?T=ld&6847<{?G|o0>!}w5Qs&yzbNSHAh)ttKK=4PBCW2W!cdt!z ztAo0jq-9tcJSnH7r2OTzJE>%b)GEK%m-+ITF*X^Uwbco<+}8)s?8}{JAGgnQN^NJj z-@=?=5idu8q)h2H{@xb*G1yuw8928X01oW@f#GHfvxGY+57L&-D$5>K6c-`AuGd0WsX^}#MkTQI8fUv|1qkgp(* zG?l)|ls#~iJr+wnREgV>@(YI~f8=Ccr!kw7&nx5f+bi??wWog`i2u8L`ZpMJ{u7K{ z{2q+G`StIg_y1Jd7;mrlbOuEg0rf5KuN8Vr?{9bX`p7Sz-$xVyt=xW{&D~dO8c$ae z0p&NuamOFfhlY-T9*2hS9`1%DPDNk-6ISI@kY~Amp7D_1p7Y;J_Wy#K|LATAeWHCu zM>9*ye=@uejOma8qK6MKLFx^>PWFp}EY1JIDktwtm>(ZPXom=rF;XD|%fos5E2cM3 z{`w0SvOW&PtPix9`NQ7b1x_~zETbcMvdmI>vl~mJ<#LJYRWnQFezpWo!)VcF z>T#bCk1*5XVM8TbAF5vSG_8jE1tqhG#x>k~nWC@>t2Gr!Y`2BxZ z_z3+gSpECV`~~3u!@`H8j*YXPjh;L4w+P-&jP!2=f1O{H)IISD-)H`S?^xdy5>k_` zCx*LG{s!N$qtWnnx-~Zu!W5-ir9!gI%F3#21MNbeRkMxa?aC`YRopo4>h48%)8jGo z?e!?#V$%Xr9>vnY{B7RCt+wT=rN?oX>jmmkmC zC=C&$G-kGt3Dz*Mpk6z=`AbVRMr86~sHGrmUeLW~7nS4Hz|Q)yZXdG_)XLxjMm{ze^JUePk`Pu=q{fp zIk#cto8v&yMN>eE`fn)2uV=?qP!s?%k}-&9v%$j!H8y~}#QRc5VcbkYvn42-fc55? z4&rCGb8rU5HBC8Po(x z%_;V{uwuL=A>^K0`}X>*VCmsgx2`tkj%7_$-wKv|n^{<>uGItht}dMc6x+OFV##Dc zI=7ADlo|DPj4mbk7Y5PtM^sXHbNKY%r=QnzJGZJQBPV(Kz&3!jX1nRT9lIm z-FZ->rtVa9X7wiabaHVf^oyeIAf^F52FxmD9?gJ>sAC-#mTeR$;Yt2afONd-|CN3&ariN7B!{e5NW;Wd z(Ce0%JtI=36tgfQgCcc>;&cl4?9JjThZV7sb)%!A_aw}*6LQXu^=L|2O*k_1jZiCb zN!m#eS>0hIAwTk?EL?@;8s^aVg0NI~I4GVJ6~0LpHWOnFc%=z`tAOjDsIFRw7$_*m za`F_x)G!>XoT>F|+kckk;0gXQlIxh?2y9#<;ZPu8#i-M?>kQLW(-a|@ZvgH>$Dc4h z8ha=M3<;BkmO=!xb+U$Vr%tcq!Gw!`d=;&6E=p0~b`7F$sFj_!L3K7XCUY1WDd$#( z_SJSzW}QeD9g2jqvv^PPsrF(MlSp69c$jAVBrk0mTxB2k>k1hit%rlx^lp8W;-Gb& zrUI%SZD;OYS@RUGW@OG2A04#|X2mAJ2?_#xo_mkmo>?4(E8md6Yls(qqR)NOipi*k z3D@AKpg({Rc3U1MnobW1@$@N8>U$15;H&_4e|1p*F~>4WIF=U&><%3g8jkMnsMX#? zu}#MY3p?}+<~LVAtuO*_L{TQLfx=9Gi6MDvLx^H;EaTJ6!XMI+T9KO%Q8YHXeb4te z8CqpO@}Z`Vp9<@zj7Tje1CNSzV}ooW)A%O1PWwsv!a1}h7?y_wVYYB<1ORmSr$s)- z$m3nTl!shyxJM9^xtYvI2qkj)YU+uXu)?k}y^D9im{>hjhtBNZ*@*^al+}3`Z;;x1 zD)SMGp_Q~sRw9pX6xAzuJ#ajl}nl&Q*n*CG-!hfH+QGJaB+sZ6nAnVY|+`D$shLx=?F>FYJ z*nbowT9z8bA!q7CPvxv5wIr63PB9~^)D&&vzudPnl0Ue2xa-GGtA$L*78b8?UP@un zCaJ5nb4(KxbQ(#O!M zT)ymRd}z(#OwPQ6T8x3!tNSc>3`GarZb}E~y!kcfELSaag7nTZiSXN$M>auG90}=3 zu2RB^$upC0_bfJ8Q$%9Df=*glju>*-NNWmVPjb~^5mra8-<{C4cQg5tw1I$-7!Z{c zz1Da&E!#utM7>Q^`Y4uLDq@{u$}*+6$yqPeVKuRVy{+;A`3Uy(<}vD>D<;aFXj;sw zboAGrwM@0#P;=jwmEht1=3zFG!+UrxH~P*6qhBoErNpqx-T5!nsnb42Kuzt;!SX_Y zYj;0yVtWxyc%~Sf#oNEm3@D@`r0)8xEa9=_i#&AjId(&0OcX*N&7WgEj3~4^z+RQX zzE2$4WVYV}dEQP|WwxK)sRI_hCIcu)@2Y|=fn*&p_mNj1fbEpnVfQCs$`0^Lc?i`= zAK8Ei13CUKi+H?L&&ysYN?s&~4=sFWQNh=%Y zv5TmrRLV`$X%Ej6IjN{IBD)`LTCt_lJnWM!yPaRdmWU@vmF(p5mV{bKQJ9pZbh)Ur zU6_Z>lhD|aQ%SgncoOS~QK44Bq_R9s5Mi|{E~w`iAlc_Fncv&bzoUxpg)%okF*+`At7)u>=TjAcgq2cJWm z=sekUTK6-7R0S{OjLMLH@hV)?mimcQcG9(3E4U`B!ltGxyOz)Vc650&tGiE-{5x{A8?Nm3nM5x= zg)fu8S(**8StgF;-78N$shKvqz#fvT+Z-sI?Me;WeP+{LR)9ggAPw<#a5pK2j|*V% z^^vpv0$FO%%_GsSX~c2jLIz?+bfosVZr1vM-&W*7bAnh$1bIg~ z*#y<}q`_@Vb6iYo+DK>6z=T=8&cDalqV<8 zc1t#JQ@B~P;&}H)df#J+yY_R^j<+2PF8t9hza0Xuitc&M4Bafp(*w;rL0PEHN54nE z4ASOc*R@Tq8BD=Pu!W3?6Hs-#un;;8mV{pSCpHA&!Yt3reGCgo^HinhWDYh{O~xEwpph7Z zf4#B&=tYS7k=H*?-#AjVa+cYn!4=H1Sq13o+w2zlN?UiT!w+1AXw)Y(e|F+&AZxC4 zEVoAbqSKFkGk9u9Qn?-X2x}aG6jX2$BWev(>T8qVI)N9j9ah-kwdjkl=Lx(+ZwZdz+O& zpip11TYAf5y2sSUAC^#XUu3KG3Hq+(ZZd6!9Pxo@5l7i)C4T1L3LU$y+aBTUTp)K% z$J#MJQt#6UTAayo<&i-dKu5m|uC=N~5uoJ{CA3Y!wIE?(uvJEkQwrt~c@0QoLk0~v z>@)ztHs5AQE6|ml^{2R@P!W0PtBTmu!edF8^+nE8>Z>`owH`fby!hI_ictk;1!hEn zlfdoAUONv1i66kZ!7J_*e4owyetbqQj;9$fk>(U+nb#s&xVnlYY$)GAA4&l=2fe@u zhNU;mOUe>foTKrjj~NTKl1{2sAvB}8iL7@iotjSR+r~i;P0>3z9G=5r2eEAQhU9mh z(}rWHxyvE{8&#|cs66bUuj`G~-Ez+!r!1&7LJLHgO6E>f;vE)yxR?Rr$$G6d%%xBE zHadD zz9s9hLd{K6^p*SR&yMKqaM2p>Cn>S`+XfidXLWC_XJtfd zka4>OL)8(P!qz-T-X_@}hwedjLkj%?_3{nJ{q6}OJSQZ+Ar9CTd&&gTMk3cmf`p@` zh(#eCV3;vnWDA*I9EgXBmGV(Hd2C{Up$L3*-H0~odr2i23w+hROF|6W*bJI}jbYA$ z$=SG==#tMA7etAGs7y9Ii7L`F{F=cl4E8I}%4p+Z)Y+|v#YIlIGaGaXQGol%lb$IfBp{CU)}Y;x3p!bNID>Ynq6`*io?YZ2KWY;7|Q9BLd+79M*|b@ z1BAu{GZ4h#o0=RNALkc&FwFH?lqgZOHjPq_iIxluf+(3)=g)U2pNeR;IqZ$%WqUWZ zk;TW5Dp!kTy<|9ivY6hFoG!O>U0ZKow&6e3`^eb!k$%&{Zaww(MtWzgIGV^(oMaO2 zleRFQzWwukZ&BPPhrv+7lEO+ozC&Wvp#ZH)Oytve4%{G4#xneb+&15s9mlHg+y}p= zI(LfpJcNc_q=C>IB~7S*`ffJvVOE|j8-cvcsuyJ+;gH!taO*f*ZWIUJN06rm8EqoB z&|3!=oK|6~2;0`^(KNbjtoNJ=7g0Op-jXvJWR_&zQVDHf37e5}=NgRh#-1x+;+!o*}EVhoJ*;NJHpsX)5F5gquyE|nhF^fs%u|v%lU<=gGeEwOkE}X zj3L~am?UCiwgZBs5!3UZo-@J{B^7MM$LQ_FTO1X%#Tl66Fz`4xXS6THO4tgrTN-n} zdyCKd1o5|YrJOyjh3qi}DW|J5Z^4~7u(evC_&mu z@!Ncg`Fauq32i;zEaI#;PY%kB^n;2$5TVM_*lUGXPjvuHsD^mopnIvvX`s^F zTcJE#nnm}T+B)}3hiF-q=cpaE-ty)g9nq22jIIO%lfz^J?@$qU5D3U9KuM?{C>AVK z*g%-xjku|J;J5pTD#w+;cYo9=wnPOL=+wBB3%#Qi+A>|ZCWIXw?t2|F51&1l*9|km zI894x!9Ed=Q$yKu7#5J8!D?Lx_;qAdDd3(7y`Hc9)g;u*n7~azAWd#PpnyD0ub0Fe zVrA2dE*nFY9@>|4uY+biED^iV&JOv%;$w)Zwx2M##xOkCqYxX$6eM`r!=@eD$~b3g z0_G7PzWWOT)2jC9`Q<6wMr1cZMs4+E&VZR_$+RQB;d;%*UXuJY=^Tiuf4L-jBQl9r zHOcDmKPJaPQWhn(O|Rl8%-_RhCHs(Oi-2N_RtH~YPq0gTz_i}MSq*VnC)fm{6Y(A# z8CQ4Q#=av>N$o+9Vt4_(D^k}BLfO=hY*If!En7c)^(A`mHGlz_uc5hX8a=q<=w}hm-|$Md*Q2lfN!GIemhM*P9n%NqwGH)pRntHcA-`+IO?A4qBK#}kxqaDT8itcMNY#Gfqs z`KNH{eqos?l7dpC3Vo8pH}E zpdL&?xY2Vgmuioxt{>wy^?^yewHO-mIhn^^t`I z`b{xobu4r_j6fW}exy>K!AgNznOs&(*Vufs(uG(c21L0wxQEwk52>4b9dV=VP)YYW zdW$ut1yTgVEMecuH1%;|w~|K%ncPM#4>NtB8=NW~(e$aQV4M1Uk*m%Pv70ofoSy9G zz+7;TxqGo&{lw(QH)011;Dq<26bu|DeA_wyaCf98yQKjf-&BatQJKD6pC#H2ZHnce z>JJ#T&NuOtlVZ$6rW7 zfN{er-GaskSM1VNb!*&ljAia?U2AK8gtbh8JWy82AUIZk&To@$V^-cxM#F>)Ue6IU zcY22v&^F)SV0-I9>+tr`8W`pNqEO+@0HS@X`Q=Y|w)2-aSpo(6?&rfk^II77pJ=oG z>mWJmi5giO+3Wpd18YW{q~bgu{E&wnG<7Wv9KaSN?sj%bZL^!7*S5-q`Dj6Y*jlw& zZ?~bbg|R>=-h@A1H=NOmxVr`W@POd$ndtKtcl`U?(Gl&J<=`fSafl~f!zlV^sshDm z(swMj-uPS}qgs(hTLZ4n)R?Utzc!CZt4vL# zDUMqLV@NH<{dwI`LytfVFr!GHz7NEV#X3WO;x>^?vwVk*wGB>`YJnVI2Gtb%&?-(m z&O{?M_wWPk#RGz@dUoDXGTkCL7GvV%*w4UPQ_7FEe4$8*LE##ENTug~ zOqinRt15GLUflA8%t3r0U+p=HrTB^tQYzKEHGU!4r|NA~SV0LQPAI6iwx`%;mrG$g zSIq#!CgzbY2Cvm9hvAq~mXo%VIP2zj-i$uR$#b8}c~_ku*6U-M&VQ5?F~PLoN5gpP zTtZf0Y0+`t)ldGYk3*p5Ye@?q2c=f_1Rq2Bab2t*en&N~e296pwDNs?UDukSDMdC# zHgzgxDwQ^cHnl3HD%D0}_I2%Nrhi7)A4v(Y@$%co=MhT%?GgG!PW%Z zJ~1@PKQQ!vFFeKn{Ko%WZ5TO!PFDX$dHVYZ|Mly?BP{&KU*rGNbp9Xm4W&*)&>C8plAC9vL7_Z8Zlo3%8Q}5(M+-5@L z_XQbdGR{-pTbJG*GTv|R&ak?;BVSt4QK2hF_uAjJtB}vuL%!3&XfH$(HtoK~U^#n6 zW3NRKk(}TARs0b7q8*e8bRie6%9MvKixEjFsA?RnN3nshN9j*iSJN}CI_p;O6t)DF zq>aQ0E%B^VSZ;M9lA7D5$l11r)N=pRnsTiAwQ;XrAy;k&X2V+sMaY01)Ml329)btr zZ9d*Cpqmcg0G<`&6@rxsvqO9S&`FgVjreB_G~O#BAfgC)xH=`oRdK|Z*Ilb`BH$a) z9cCNboROzIO`E1KC^QaEo{Cg8a1NC)dC;7w(@sdv865d5cu-bG6op1~Rg z`~h)REPLpnc{R?xprh;7lMZDt9_4}=f+-?gJ{Xv`R7DX(Av1hd4oxb1Dhg!|1i#WF z$Wrp%(?=Oe5Vzd&j8$@LWR7q?4QWBEJsYC(S|`dFRQTS&n8#+LAdN3V_&dt^j6ZzP z&Z!jN&j=?5)G|ZkPHd^a1(8r`babxDjFdeFD+JdX_fcNMXhnGETLCcw3JDwE*XFFi zgafi_h9Kfd+<7`#B{@D$FZHysP|HyBS@4z1^_u}ci}Z_=tjRV;U?Fb_F5(J@Z>_sj zx6hu*Kg;B#bE(F~;cV=a6B)WaHeG zWu2P1OVe*Bl*qv59?)`nh9cF#crIZHyas|5T;7SfnLZI{=g#Hf>PHhEH;SAaHN7v% zqQANEo4jHR#>Q;H*q#N>(VqpGYS9&c3oBt9Z`>tHzK)CkwtZByy^9vAoPfhKsOCME zmwALFP+%py4;`*pVRLY}sVQT|M{EHDaN9z71 z_v3lX)?rk~Gz)i}7iV0haLGB$oB48oed2Q*zKq zHy!1XVw4RJ6O3vg;dE<$)iH%+6I6*Nl%&X{>I}K<8=*?lEX_oPC$?o$!vxYgf$1Dno>T1pO;v%R(gc@BMVaPPQWi0@e9Wr8m&qr8CQ7*@q%Vwi)#nVz4$& za7SOc<`!PldI{Mtf_D%RSMN?41&&>1mQh2Um#x`no%4-2pfoQm53mT3cWO9MYfeX6 zD_PBvN;7romosENNM~XE=z6Ip8-cAA7Rn;dSwi!L#=PD*#o8}(Y=DWo3 zc~Yy*w^?qXd-39wCW}tjSHnzgw_NoD?M3uqqdlL$rce00=ra@&QcwV zZ#egtKwt7SFs6M{j$$(^A%$`~a8RR#afe}{XJ2jPbD!fPGgKg?2x0gR0IJe4m+%%r zOc1u3Am*B7t%|}6qLwB}0wCACTz4*C{K?%obckd53#j^}()mwqzXJW#`xYl3`etDN14rTTpsTdMCfNjx)nIO zqS2@X@#eF*Wp^gi$-bX-IDZ4N;sx2fc-@eO)1^hOCz!19XZ-33hNsJ$P5xQ9XAADrbGk_5ck^HzMhXmgd^oT!vE-=E7 z{A$#l1K<(3vkzYtp|72zK~VBE_fKF)55W+EjqMW-FQMy%9vFpqSyuX*%)AaF@7|!Z zZ041<+x-rSJN4swZNJ}sO)Zql?qgi9B2THD5JVuOz~(I0aSL-+$1BWUNIlJ|)Dgx1 zYPmsH^E{R+z z?O;WUkLWevXQAs*>dIGg+y#8@Mlt{u`4iCsde)zNsqBfB1m&d6931~Ba{kKu1u9w&ipt1ZMdDcCXr_WWdAbmy!=o5wOpx)=&BA>`@Q4^WIG(rt zKf3XDLeA2$R_TzHEDMib$~zubU(WY~yJ)mPs)4kjVF0;l4TC-&xUIc zkPDJy$qJARMVe!lpn~VQY&eX?-81}G_SnbZ@`*x`P)sxFWYaL#93@f>43KK!`=^Ox zQgkk=IHK5%B?0PiY*Xgod#ZJ{xO{*f}3RBS6F?S_<+ zm$9|KdqTmLBHn%RFdr3p_2c&u0kp5lrc@44?J%U0zfc=`;Tf^O)=O1mg1;uVT|j}^ zm9-9PY>wsM*EXKK#MyX?Z)ggG)v@+|hdd1z)S7RTw%zpBUv1gUjV%uz{lfsAxKAX-V_uRk)q6 z((5M)ugh`8UH){x3lE^;BiV3m2t8lv{E$(fqdRW|N(^f(ORU z`Isg4lQXGDxqjDB{#|UtMLRZsWZ!hXNLcXC?0fuMV~*eRYX2?N{zvxxE8}*ksykpQ zqrO*?NH)&e>BpzAt&v0NjztZ`E!-G_56UpPLNxAf2UHMkuw{sCNVtrx1a;F7ct`2TRm)djt`;2Rwuj4-_uN{ z){`OUy3svcr|iVIa7!|vLeEreE`4*TJg`tD;C$D`*cmyO3fO7x1?`f)m4Z z9{5IAM>3Z=bx7N`N_{&l7)vy#yv9PCRNq@Ct;8HY_x?JZieiSm8H;f68H`e%y1X;8BX{7uVqBu{{U%kLb*1DQ=UF}P} zYPe{SjYE-oL01hB)LUpbwiv%~D|~S%2-Z57;isO)tR=a~tiRr6=>{qKA$xQ@eX~j0 z9?LZ=tcm7nd{b|RCb@s{LwaVr1N1?!OF*K%VLZ-B@`E_%=tRkP|4@cGkv~CAJF+~S zA8@iLriTyl4RY$$5gXkVxl!#yVt1raczs;iUuzqF zBV*~^Z0L@RLP$^2Mkk-z*mJ4&C_2292sCOJ{o?SZon~c~cdxm#cku)iqa z5RZFaHr1{nVv*WJ5BvYa+B-&9{%+aA72CF1v2EM7DmE)g#kOtRwr$(ClZxKdIj7IL zx8Lr6pC03W*dH?X$R7K**E83%H0K%{IV>c^YDtLT1JgP9{E+HD_+gMBBO6)EQR!6zmqIMbW5cyckXG2H6JP1teK&|I3iRV1s68 ztGqDNy@QILNI9Et!t(rba(%D`s8+Bv)GT-*jrhqC=0hBd63TSo8RU@fW71=W<1+GG zl2loKnBPe?d*Ky|WonOD)jCxsO(9)i8elEr8hVx1lJ3dK?6wyXXBCB1CDyO+Px^g@ z3H@i5RAsuD6ze@U!aHg0BY8U-ZVtfLt$t0=?GY}^`P`3d9N@bhEw-;upda2egjCle zI%1uQAsi2)KzPGG&6eOpSuk(H;1}net4?Jo_5~*Cly$5oBYAqFhDV?SLX^Yw&6i>$ z_goY0R>A8Q%Y9JXgSy{$O=E^2wo<5XnIJ@Xq}Z&7L>#Zj3%wgLZq0Z60=I(1QivnW z1{22_yiOGz$9|-FGQA+}V>S}?Tx}|2KIVZ%0c*4hiZfF)xC2KiM8%eM6xwj90cE7fR=;D_526~^ z`gaN=8v3NdM)KhaCgi001OsI-&w%;Xm1ZPfXDG$J0lcFP0zjUBfVMHX(6cRlvg4|5 zKUMc41}5IryAA_MqN}F|Q-h*DDXi2r*p-_Ip-=cR?ZPv<`g4k!P0IC8`Lt#MG4l>k zeY{p5DIpfMyg6;u=V(MD@e84y0Z0SuaN{7P7DGvIw`g1s()Vt7d4R8ggXd|+f|db_}@voo~)buOCh;2_AfIv5>8o8f}R!$+BrSE z>LFrLCZi;uCF&K5hk?{drVse7aQ&$0IcJz6A#VWsnk+$tRF&5-{M}{RI*bk}P5b1s z%bS%mdoZRO&e%$aaA>=y9X{biO|>_uDu5pBifm%W+VBnScp_|h(2h(e7l)P+scILU z8-8jwr?K^iAoG-SETMBFu$zV7Z(Ok0_SW5IW!ZJCdr>Yt(qsmQ6lT*_YGr#gmqXzG zF;_v#BP7eUbdBCCqgsELy|A)f-9gyp{#d^vxg2I|;`ctM??GoLUR64rXci51n)RL$ zN^0y^VY#-#o?f9_-}B|Rs0WW(9Bwfg9fIyzi9i}S739ukaInkwL&;O~AjFn{eWVPKzKBaq8uqqAvpqqn6hj@#H-Nq3O;!(Tfd12yv5I|w4FFvI^MavxAc;w6%o#pt~^95{Jq4YH3wlJ<)At;-|J5}XU&EU(3S)M)JAfBeCdEN`ks*H?dp{V)4SEdS=Q`~PD~ zI;;(*m+wo}EppkCtzGMCKd~j9(MSm7;NBjd;$lt)h!N2|Ix`g<@`F#HZg`(rv z6qD88N2>l4Dk@~9CBN1|c`B}n7&iRcMBy?;aX{cwYLAB$xSyT%YqH`fEE}mLE(!e{ zjoa?|!-4;?2j=Swe%X6Ip2lkP$lb-o1F+l|8U&eRs5A<2l*EB?VClu$kU?0g>VDWzZNgL&$2QVf$NGfVKpbBK^CfJI@1&vN$Buq}{f>lUF*4Lad5PI7n6i{XQmc+U_n zT$NfsXrc4%lQ5*)txDxsgp|$QjzgFW-lEg^zj0O0zXk67?mUAST7d7L8k^K17R(I! zO``&!{JURj(1q)&C*gWx&QKRit@LooXVj@=O(n-x*gJOvUc&*+>8wDBK}&yLmrB`3 z1I*mpnjshZVa_P=)-g=J#no=4a{NPn+iQ2(VZ|0&H6O8UVlW5%QF>Nwe@Tye$Rv2r zh>|N-?MKCJ)>EVtB*blaEWfJ%*=!5n_i6qQOxpo!9Vzsy#arfJR+dhjKn?Amgs`?l;oKVCSUF@jQRdVrcEjBnk(NF{<-awp}6E+ zaf!HqVUc-JAXh`2e)ayo;6U5XN;~Wj%AfU*SWJ46}!X=t168ZgVWu9Dgn}z}8!}-~f z?cVp~@eqSt$CvLD9+z_J^7gHg;;nf`jd?~O8z%nBnXe*@9RemkZp$S7r{FaPWf-SX zi2lg#0r7|d@n$sQVl}G6Ty#)L;AYz2KeP~WQm#;HlgazVosBwGhaL11X%J>RQ^^oa zZs7iSr$wb;-#;555598u|Ch&K!Pvp}YXhY5KOyk<#15Pl%`J)~Du?-`Ce?YANE*Mxg|c*rjD}e0!f_xope= zE4fH_YN^BR7;TP8;qXq0UC|4-VDit$x5ttLJ6ltB#KUrEQ<>}~R_SKdEAz#3s1h3L zpf`Py()HQQk!K-gO&Z!q5}=#Pk03j2Hx%aE3$bg7iw+rlI7K_| zEf!${2lOIhWF?J_GJ5Lt@|)2rps#aqFBnp{e}IGJVK}KO*Oq3g?5gn3&DGJ2tUC9& z7FwII>ofW02o7Q}1#2fkL+H>>B07eOy-xe_Ky*NJe@+Swvfne#7qKgUN+e7ZDbIob zaEnCDe9-@fCx&ix%sn9W)xVgUmf$~}AX&0|0xRp|RF1XvK`du#mW0C#ghvL!BDm~{ zae_b#`QXed^Tvp?gMfTkROuNG*@kZNDCd5o-46{HdG-J&AlLUL zur)e3{ah?Ayh|{!jze4QZ((@O6H%C`ECZ2l=M;!)s7QgnV`jdhn3JeDA6Zs!3?uRI zVBs>;TyP@vlK;A+`tunzNZ{yy*hxxXQkA)-)%d#w=A@DaTz)$ZH?~eSPu1b@B_dm! z4x{UPCvecS+|D#Upomvys)BR{F08hkV zZP+6U=Bg`#JQj~J!x<`p!Zt{ktI0J1Ro82%JP1TZSI>qkBa{<565OT?E1L6@+)%q1 zFu#=etdGoJUrogOK#n>Y&-Ex9*!K#jY-K9-t@5f92+@KB(RGutDWSLz-X7&cT=eDeFrn({w<1Dl z{?aQi$4D#jX3$KD;jhgl13DrA@yb90KheYFgyE*-j}iqCIeG~*#g~nvH|#OoeOs0o zBpwin-USAeo=Y7|0-mU3`VvErH9`BJa~2u$VM}g(f}Y4fScAn}N9Z0t;ftf~^3=p` zg{8GHfr_+%lY|1%^B>EKc!3^@k+u+47ecCnJ7y1rEr8US_zQn9b^~5V<5VNuHdO=F z2P}UF?$2XnqI5r(rP_s1)a0r%rZN(~(jd^8PzBC}Uev8pesgJL$uG@lC8D(pVwuQC zyqP2?=$8Q}jf5bYNC}^-&;dfX;RolYZcTDA9L}~5*G`U7(ryT2eR)3;OuYxTtXO^d zfbQN*lye7JhB*+L<)S^ru>M59_C9b*q4IHRNot18HAbIBaRPoQRDKKeZT7fhFThO# z-z;)NU{U`8;L)3AJ+7eA)X4!Y(3P7u80OLku-;xw7;Vp-qh2L6G?wM zRBj3MO@-CN2Jauw$*>5vIP@hdH}{uAng7FZ!GB~kwEv3h{m!UlFY`Ulx9_CDKuci zYR5f{krJ(yYy1Gsu+I(e7Jrr{^6^fKp=T&zeM6v{4mUD>c5tY42C#`%6&6Yw9bHMS z+ns)7=Y&6>#O+CV_x@Pqq+uKULf-M1>te<%tE{}{3QufyxV<`fzK(+vOo@&6^rkfE?fYpfL3yg`11zxX1Gk+AUxyV}T*Hq- zdzQiIFqxThGo`!b^{pFdxs=bRr*WU)XEATEUcUTKQ3(U`eQ+YUAwjO-fY{B;tThMR zJIHu+s*5|pOAxK!n`b>du!f){dHs4VjFJz#Vq%~4eh(c&gM|H^(+2R{iWJJ@5!oiT zZH!7eN8b;?j$cwU_v-4G`oymJuy=|jZ;6$8_W1>_EZ^c9IeFw(<5dObQ(uM1N#(n< zIW9tb`XHQHZB9TwaWwMQXJh|n2z3GK-25qkN+nhlkNY_Qbr}rl$qneco^Y}FOZf68wU)zDNg==IZ zJjA^cBFs%dbsp#9i#8D`i~-6CFAaYWTTZcnd`7OKW0g6e1u$}WmZd#`bgP>LbY@T7MaF$v+nqV=7kk*0 z9+KX={qJ}CA~Y`=G%fV9onI7M04xf(pi-#9){X}h%(_AgR4arcE0jf22!B5`rSeijl{W+zbT&u3qvi?eg#z+e;HH>{jYP1e+=0E z^N{`{rH~!_@({?}O^p$Q=AM-c87lNxJ;DXxxr9Qj)Y{Eyg-Om#DZ|9_H&t2?G;iN{ zCEAhjz)uCzeO}p~)`#4Fxp@Fq`;ow*2n-eb15MI6ag8qnyOit*r4g)o+Yoh*I+^=A z0LV&SC&Or>;Qfl{Qi5pnO1oK+D!wwyyMxVXz||?XKj~77IEz1XP5Xmm^~mw0bTV1GRLI6jlSD6C3a}ZW!C@G3Qyt(C2;2I%MgQWE-0= z*1Es27NAxTCLK7yBY}Uj)>saXs&6wtc#>YL#IeAiImI18>oMRSkpXsUURquM*}dQ4 zkMeBbc>}MZZ90co(K$2SEyX_AN0rLZFYJ(v)_6?bhNk4NpR97SdN39;vy7J^l3{`YCFO^_#mX+c1I* zaahaHoF)PYMbc(mEEg)ncE}JO*&7X##Lp#-_S)SODkxfY%tz(LI7{rCZhjxzYgUOX zNuqez0OS>foXuOCW$rsd(xOd#N&j@`H6no>Es%9#-0vvJJBS>T;al>`Xo0a% zYeEN8wlod)v7*s?#Vnl5Sua^goY5L%s2^Q#%0_6csQdWaCT4VCEHBQ zIO4Z8`*<_5sLZiLNw5Yp8*0|X0+1HDRi_+V0J8E!Ly7+w8 z4MLz@*Kh`L@!-!KCqIry*dLjovI#+;YrYZ8sjndrH6D3H&Rv@5?k_fhI||&ZCQjf3 zom1QYEPWD*^Sark5ItERpLns7Pn%Imi)c^n-u(`78G9Chels{q!le00-$U7xEVhJ_ zx9z~2O5!hhpo5*(kHnX2IF;^WN!b;(v9W=|LY?E6G?H;X|ATK zXr5K@yr}Gv(E5sn(dzIO^Atbh`&zCQR2HVi<}1HZf(T#tz+LC{i)+y8A;fo2rZ66j zPr|x+y8i;G4s-*e?+rx7F_Zk1V5inKe_dRt&2yCe+1PG2YMrfRmBrA47vtVai@mUl z>O&WeU;a7KdDnw5n|a?>`ACwZ)`je;&neJ!k=L!*s!ft_E>WfzCs+1H^*M^I>w4K> z%2wOzDj;UrLY*RvZAc2841D(VYik1o{+2XW3}Nt0V$4sch@fMy8awiWCBT7@3*}Id z0YQsAORnP6z3R<5kH}NEzD4`+%|d#l@HoKEe81jbKoc}b@L^lqU%}XJ-o8%Mddgx{ z8zoW;mwnLOv=7h*f3gdT0JYLbKD@?s)Y%;BC}4#;f+bbGj?MDw}VQ z@-ERxre+wiq9YZU4@k3{iun{ut+F+uQ2-kSl(U;(SXSX0nI$+;0+~hD(1is9@$u`* z=}ZGTgiTO6iQ)4c>xy}r#S-VfUNtMf&u-a3Ax{_aw2v9`01<8mjKCPgllXxy3RlZY zxTgJ(P*_HFF27En%Qq1P$bLGimf}H(I-4T_Jo)aWF0R-OK_R~1{lUsy!ABph-aGJn zya#06Ieq{jgMxAWunO-I1Cj;bRr+wBjnW{}CaBI(#o>)30_VnewvIXe#d(MUOS0Iu zo@E5B+LRu)RLvmRrv+&lr0>>n`4UlwE$A6fH_UjZn89O-s)Ab6w{-Zpk`1k5)p=z;Y*a0OaE6S>`Hi&@~u{3l^ zOnz>Tzbbf( z3IO&&^KDES*(bZKCq_C<_^y@~hI%Kp*q|nArhwL++av(py;NxTm`<)`53TkP(d^wrgS2C`V75f+cos*zto1KYOXeFuNj<*A@4$tT$d6wV3{T^ zZZ%QLWxm6)adwB|G7g#qnMRtnj;i#6JD0XebtM{9NkCjT1)|CfIHK@^%#58Kh)t~D zBgU01wh}I7!kMd zxk31m9(Rm3QqLvPt!iBQ`?)_|t6M8r^s#7k?4m4P3`GeL;)M5rNTNZ1WVU=3Vs zYJ_wF0jI#)j-}|M>yfUnKK|DI9k2T@*YWyW6!~8_NeTbbiT(IHPwp>Tq)h+b z|J&O*+8EkcD!JJj{~bRGie`#m?NCp_N@+EAKv071kW?zsX6|5cAu6gq`Gn}w<02A_ zzFia6hPE8a4@wXe3CYnfGS671bU*xVP(CH+9QV6U*Jb9*m-+>}H%LCGAONuNQ`C(B5QNOX^8fi|IB6HMrx`z~mVq1kZ60B0M^E31g3#aVYO;AGD=HwFsz zX=XtO6?Co|*GR{ral)kSxM|rzpFJf3J@jb$;4|nCBO*4`w$h^oWmT^KgUM6O!*34t z`G?74;G+0$pVDlW`Uh53>b{TbLcn@RSG*Q z5)CcKkLlrVQql0Lx09TwZEbm!hlV$iMcBPWToBtg#sJJ~JH{BY{3u|`V+s}^bqa=Z zsTP|ny!#+Wvqqn-#bXBWQ~oNg@L?0x@s+EO_HCQmzewXb;_1s&e7ffQt6$TcWW&^E z6K5m>PnD=SbZvUbIIRXoZ%?V1mj$af+Bm4JN&fJ7z{6ev?<0cS#2T|KF_y?XgUVTv zsed_a_1y00$TnXg?;r_j-3798ou|rg!PRmP1smON8VWIzp}z_h)2!#(M5U>V(AI3` zV>fLe-4rb?4A+U%>mU?Jg=g+YGCY9nRY1SzQ%gMqBoXY|54&!w5IDn?z{1@Oz}r-l zni|*tj-NvRbmc#C$q=N8(1J6a2F}sT1<17UCOP;{Iz_h@8pX0h!rIbJt5@TrOQ=G? zX&+wmW)WqdDH}>-<(B(HE!EQ45X}lA^vCqJ+%S=hpnhRG!3)y)XEP0#OIMki!_vdw zYHhr@421JvsN4C=gqr%_LXDD*jitli;SwP~V*Lj$D71Ufas4T5#OQUvVg^yb4m&3O z0K_!O!uI>%)>dtH92ie(j2GZH6doX2*bu}#-cbTdP1H3Q6b{8p4IOUE5AQ=sHs5dw zZ>t5^<_)L}Q?g2ok|lAx#2T=_bH}T5#cG%khD6OWjc_AFEGXL1>76&6d36WCwLZC`TNwkGxJ~H{{>xg(%lgn`p#u{B;I&ze2AuU?FH-8>}n-64ZwPb#YRqwQ~?g+iI#bA7tc!4j;vdI zok&TFLl)6|f+X(l7n!x`(C2I zJOyUfj>h)Z`j&s;e0l!85BOIFK+)LV+1Osr-p1AKA3>)Yga`5h&WBFz3Pc)$k&hhT z01!Q~4~no4FgrAWN*p|rZ$d8HdE`jL3vHDrQ9EV%~$f zm6b(@g{=G5KskXNj+yt#TpYZFlwavqr@Bw&i6xU zILLVzUhdYbT)-aTM5_WcD5k}F?CJi(c&oIU`h;3l3}-HVj)=NNNZ?cjJ4SzdrITqn z3cAXH+KEOf`@EBKdKOYCA6rRE6XHe8NRqCOLB7@@dk{>I#m6SKJU;ToAfY+Da#TR& z#53kJJD=NB^)mtZUgDHOJsZQ^5aLqGzj!ieQV zmAoa!LhF{XpA{yWzXpc%I3j0l^Sj0SXb~kz;wU&3>gU~5T|Dqj|5YWuaWE*@cKaF$BA0dQ;ZC&iRlN;5Fr@^wzdI+toM9-8BwX}jU~>J9*|j3%A&Pn+C0TX zHi~H(1@3v$I|*eV85x=*usMFAeqML(SAR$pn*qAkgVa5-rD%p=1-j5;1a`k#%!P6x zgN|t9JQVsNjw~^7+D{tOG^eBwZoUQsA`mf&BGJo3yzF=7@GQ!>NN7EORV1}-?Shpu z$HrJ_>|}H4Z((EJq{(5n(#PKpp-rikl25|~M`z2SR)BkVf{jK}tkPuktLBe4HGxnd zGT6pHMdCU_C@qIbFlkVeJ^7d&h6R&$83q-{=_Be}dc{c!n1ON2Zx|IFo1dpnZYFc9 z_TU~qi2*wh2h%M49>qVfX?#y{bo7KdKx*PecTgd(?V+=}n8wMN zp-$x>Pr)B10iUc(OnWG+H_%2zsoAMY54MyVStcV1 zvy+zY%Gmy)?tN-+eL-9L`s;;Lp6p1-S`OH*1A8{xlc4}o3tTOYLu zSd2ER2?ZP5%U`G;vJEz(jwQUY6waMCLM#*bFV5znMg)&(id{Oi*O8hD_mv3;P|(u) zbGu4^T-~>>Hdj{kd!-b_Oukn3yQqbP}#%C~pTLie)*``Oo%dXIN*w!8>R8lg> zVh|Q))Y`Y`#Z-8t208%eQt~l8Y#*M+N6|xzGgl+t`F9&SZN0<~%D#y$NXf*B5|U5e zbfP69cOfeo#WcIQg?{%?XI-j*ad2g5J7l85bXAaV8L}`5?NK{R2Jtswz!qnhBFC_u z33kBn3Kb6Qd%)PeO{eGN;R9@Z9XO#k(J|#rp7uq`q(=jWw7bJF{qvVig$J$KyBY1rUBo< z+t-$MU z*|`Iq#*w)ltU{(uQq}c*_I0p%V12$qP>s%~t}3V|90oy3)RyP`bc?vKLH@o>Fa zd71mm@vKyN>z!SOHwMi|{rn+4dvOO{ME&U5=x+7E^8D~dZYybQ$zVYMF-UbQT{ z(_EY8Y9Ndmw)4v5$W|YT+dnCS;M>n@`cQspN#7O=2&%UQ>N3XdSFYAce@!v29?(Rg z6SB9xEe_FiDLT8Exs)weAeZI+=H!ZZXlA))(#*;U$t0;gI_^I@=|?;6M?2|HI`03$ zzGpmsDDyLzh|mpb@==N{?aHeg&y81VhPb2>*O-z8#%-;K-m5pubDcZMvzFYfVW$gv z+eR=Gy7W*L@-*>~n)Vv*EzbL~VYX|2{cD{hhu@vCgo)6Y=d#1%#Pn-`QruW%%+eL~QtkAEWA_zPOj7)zasc zNi1;mUhX9p+1-ck;8)6z?IP}7kr=Li*l(l`pVa-&nf+_az8$s!=h+pN zn(pU!VC!Vss74wYqacyM(u*s7?}NGz8!?cu9sm4W!80`bSiT$T+qW&;ziJoq|9iQ@ z-x{}OhF`1QC+UEm_mlwNHlP2cGE8|tF%VfCj#~P9>KsWt!3ZNl8rQarQgI0v|D^O=KCKVny z$O5j{bdkvDdrvYZ#803DvNC?=T^n5lNYI7HWL{*g3z*-4=0X5i0(L(^u@=Y5ttqGb%dWnb$_N&S@8Z0x4YMh)54Ye&cR)3553rvYzt!&Dj4Em_6i*#u)-5nATP)3l6JbLP2wdcR>-CWRsm-}UXq3W% z(^kV<9&0M9N{(4ZLODw)+!UzRk9WTRU`vi@=#I~ZVW|Yf&m0)Nu!^NjgsHD$v5Xb* z7(mElFpC)>tV9cOq4fhlUuqE}Sz>%EFYsr`(lS6_xD#+r9U6J!0);FiS9 zVwNLItEKQNL{&?E_z!XfC6Ep)fJ{tD_Y-2lDwsO=@Dq>K?%GYYB+T%awT^cG=pz_l z0nf6InM%+G?%cs#aHNht+gJ;5MOO-^932?fSBrTy)#0U3I=6UZ|2@$gW%uGDGGwYB zsXLe|AnxwN6nhitBl8RT3M@%zuDRw8%Xso;q_Pg?Hsg5Yx+f7eV|N(hq~M;f=xOWi z`P|!ZzYWRojgXU=2$og+JxY;A^)dk+LQI5~A<2VY_6O=Zd~}?wz$lcpzM}69b#L4H zZy?yvI7Il8eyC8k=F@h7wLSc!?r;1JjZH<;5lppFY=nXrn|yJsyGJ&j3i)h838%4R z8OmvE$|qaJfMkT}(VNZYfOT9c9jVNKphzHz1m#>@xXAVmImx&>6zMAXN;srNJ3O+r zr2@&M)KU~`I(smV{)F76HrgFS-2`_+3(KKKJj!%5;n9SZc<9{>j#F6sQ0Ot65%4S`<~xwmy2B z832Ydh1P+LPiId6nwXaX0h;t!22P^Tf!SvOXV7ko+M<$}GGnY=!90U0Z9Hhy?`zhF zd4yTVa8Bnv1O{%hRGf$T;7YLX-39hN?0&vNbLK%ef;VGVW^*g5Fxyqcla#Fj@QMtd z=MG{z^{O`5XrMZDOqgLj*IVo1L!dF~E&*m5fu%^)A0BG9sxr6#F5{9G2aVYRjv$H zqWbJPAJ7iD9%8$@6SGt{Lzm#PQ0!EGs10~&!dxfNgCc%#g;0%@D2H<+8liCP|GEtT zaw(eZ;u=H@(6-IL>Q;og_bBPvGkPE7e!ENMrsnNUg#J^AFEUX&3UYXk!C8!thb_Gt z3|Cc;wl`f+(iA!XP#!?Gn`gGEuSPnwKV`Zx z7hI zvz>R<)U6RUBQ>SN#|eRVp~_$OdA+u>vsJ;zi!?u;0uJj+7nloOCAtUgu1kN#ZjnrN0Q|Ee8Y$hQ>O z2dUbYc3KVGkXR%_bM^~cH(K`KsQyo&E!#t30dD&(s96&oOjx62o1g}t1}iLM=~v87 zG}~_pxTY&{pZDtA&)UBgj)sfddOm^C9IO(lJGoPRBZ7ONT}hszJ>0{Z?p<*yg5MBh{oK z>*XY~`ynAUUM(amEklO7%LXz;i2d3%E@w{3hMRLpr#X^C6L|ZpieGxkpk>ClYa~8?o3`LMHukp_IXV}frC|yTyTaVtSLz< z+jRsMYW#!047TP=if?>ah0@wjP8_xHYUUZP#K{zt3`LcGZXvLipyO;E(J#>ok8qQF zNKfBaK0x)V=RPy>@16ov#r}NM?1Zs!MG2bSlGhA^#(+9W3ou6zO6aShcOW#~c_+Vm zn>cA#wq3*U#j{@fvTSbecc)}*pq9NWF&w)?0g zLM418n~#<;v>zzMmdet1;NWGnAg%8G#(-%=Td{xqT2daQ*5{lIPq^AnjXz?WmhDu6W`)N?bq8?$^N)zgU%^A2=P`BxM9O}Nm( z9{eK2GjJkXbtW|X_tMX!xsSaE5rC{wmH=tM&DrcZkoG)&YQUvE3}Wy8ksxST)~VW6 zV;)_hh}$^@bItNXJkNsJ5Ugt|5Q^j>jS8syT7LU`#b}-}P8+mi!A^ZgdxAO?)j44x zP6(&wOW6Fk$zt`E;gF5xJUzZEiv#(F4%XeMhomKVL7Sr1*{u>0L(9f((+w&ZZIU1d zwQW-3Cl0ZKgLnF0akVH(<+_u(Ar7>e(mT6x&%nr4x-u4!)}(bQ0n}F)$8tn6=XuXQ z3|ykyEv~q3jHntF){An_&#G2#$Qz-&h^_!SV$?B|VoF1}x>{Btk)EzKU-nW^7{P*7 z6vB^x&o-?P9Q5hp#hysXE1G!0)=VU#hJy43#6 zvHt9ZFUK^21l;f^xYcQ~em6UAi0dna+q=CRBHrU88+``C3FevVH4DfUrn9HC&Nn-- zOQmaE;G~)~4Q7#qmb9t&7+9M&%4?l=SJv1ktM7eY1nkUva zt@7_d*NJRtt5BcwU_O#5p+Y*xId}+c@o0~n-SU{{Rh+bmPvQWxb^(bMprV1Y_>0ZY z6~Y$FxeCaIQ`}0_21tatZ-M3KB{`wTupG0yf+s~gVzN)aLq386-le+|@r;pOYJg-F z8Lo@5jy_&?qn*e#jGh$@$|FOeyeQd!Yrtv<4%LrQjnk3e??1X$|8NUoygJy&Ft{iE zkxp>U{v68uQ+oUC8T5di_p0Lh@z!FbdYw6wH_%RUGB$0sgFkC)sDo6#Y)DhpycF&i zcOq$i2yy7I5CP`JI2Nhz1)Z2%#U50%de(-Wmw}tMY}=}yfh<~kLvB)#E*~eS+Ym&sKT-z z@>OEr-M@HvQcWL0J=m4=M9vM}aBKp-rFIX_3R;grk6&|-P^VDpq@N4-p5=H z*{7FOP5G|ae-$CnV&)CVbF^De$|6NRw#M6AJLGvr(Cv_gOCh9Q~2$SxCvp4NEMU-T;Ut%R6+(%y5qaS z?FOzw_QG*s$-UCWe~VS6--F2}Y;FbycM&qm)^&8;-p*0gJNu5m4q_3sJ)f~{n>0Hd z&Pcp>(~p=P0xnBLP!zY9YKC;C=#Pvk04eTDLvKU83~CrzJ#*D6-_Lxqn>xPB0;+Rc#<4RaC?d1Fuq2nivrZb4sieewOcsW z3u`s_pfS`R38-2aL1m5nkJf7BWW$Ghwd6Z4eYwbMai$yB8vaX)%s_0fpL)+cp|`(G z{T#?|Cxga@gHafA51e7hEa}s$2W)#$&90##YI?rDfSk+MAGU4&G>aM4!kFv)h(|pS zwm|GYbyld`{D_xI-ZUke)Spod6kY-c3U?6^N-grFa{?*w-%>8w){Q0$B*AYKTJUlR zPG0#x^7@O*8`IRaWr9`JYFi^l%Lgfg8pjYE025ZSdT?S-YB=Vo41PQOZZb>?xlu9j zaDv>UilAS(YBP4J)|0Ym!n8>JVEN!o7T$Iq{SXspeiF8wEa+Z^MMBIu%$F!1a8})S%c>~be2~dWWUvxfpBcqPO*Cq zSTm05Q?yP(4EnK7BZ{5r)K*I&9b2Oz$&9*98akIoVcsO4wIQRuk9wNbq!_-*PyZ8b zXG6Me9pL*mZgF4+^461rdvn(5Bao853qKr}Sivqq=yH%^=zupu5s*kaEV>HPV2K3ozP?6do40R5Sz??g7uKcvuqGvoV&3Jx6yVtwf zJN9kqUJL^putbqMzDZ8M?TO$+Hl^Omjo~{Dz-38rnUJoUh<~Bz)ZuShk^WkUeRh)E z9UAYqQAh03FL^96*?po4*{UbDWzvv3ymn(8=taA~a=E|SDn6aP#&h51-Mb#@xZc8$?bY48 z)}H0NO^-s-ql^qnVM92aPL9{G6yr7}XfReP0Y51iUG6s{w&M^_Hzz@0gmup{<(%jF zF~jpCc7BD}sW3nZtIJrkxo`tnUd?0&6YDc&%2$mUDM=II4zHOim~*24jymKbvHut2 z4WOh!kex-DSVw~suR(5QZ6~K*qO+l35pz~_%2cV~^+@C4oZ+FvEx|MRo=*I};1gk2 z%G4>7N96RSYO-O)6e(jCv)QG}oRg=0#Y0HyOM0wG7MFIGhu@S!1{}`GlR*!>hmpyF zxPF3h^=K~yM_I_C<{9H%iFjVDGX@%#yiextBBxmsE$zG|i8B;ko?KU?%bjkhhQH${PsJm4Hqv`Rh3Y)IbFnCd1C zP2jyaBn{1aG^TXQD+w}|tbue1GE|-68S#wmGPM4Z#DRwI^u%4NnLR)_~x$u-q%Y)-kwQo?&}$wxx`D>3DLY zTirFToEdVmJIxO3R*SS4lzYFHlMQF`bWMnX-0&^HEe75nlT$y;5ZQc zZW>4B3+ySJ0=4q}7+0lywPp_|%6T4L)=&7uYc7%iU*4i_^{5~Dz>)3$A7$?tWJ|Pe zY45Ua+qP}nwr%gSZQHhO+qPY`%T?b#`1IuAH{{-Mw_y+6qwIh>shsMZ`QUIz zJh2Tv`dcTR_2teZ<1{$i(EJpXTJRkI>_<-rzdqr99xs{%;(l+0&dyIkdD-)Me5g5i z2d`L8fcri-HI$k>SN^6y*OdUH!d(>-kIBf@B)9pso1fh+_Wa>PJ_Kg~`8Tubm0@aB zZlC=Vn)1HYp3oOy(qdZ3{qj!Ae1vbn^E1Qx+su5lmmj{Dpk{Y~^b@vb=yqRoNp{q1 ztn%!+V|N68C-W6GeG%5`e)X00j_Bq76}Cs}y-lB>N6d+plU-)u1%eJqlVY0>-yGVy zAm}@eS*Q&W`<200&?$os2Mk}T9|n@w{ud)#!uQGTKX8p$JYncXFzk!Z_kr%=8 zGID?d&^|zC2k#gIcJ%fJu%3@$1h#|>{_gjz4K7UV&SO(&{<$OnNIhV5hJerx3vD^S zoV3#O`&r@pb>onnpMN)IDv)td>S98?LN(h|r>!`bf&{`Ui)IHH)4Dguv`DQn3*z@` zI65|`*p{AYSD3vo+1oW6oV2gJf7AA@pb^|v@RYRhz``DETl>?snp2|R?kV4fIumrT z;8c`PCy~E?i3XVsxIt;(^ptMOL>4K$^|c{>PpX7w|xIdK5^W#D}R{7nX*e$Fe! z1_hHTadb|99`{fQW>o_;uw^$I4Jvgh#BDnOal~+&U0tAz~Q7|uf{{V}@ zW_YM&rKJKbgmLD0FeAVgxbJhXVo%aE%jWh{|F=bUaPRH>Xgv#W*9#idr%7vI^!PWS z`J{eN-A;b3CQ);*`seBpey^wcXbMRIJ=hnts$6XBsQ8Y=P~;UEB>Ck4hisdpeQ^K6 zaM_A{Ap+$v!;n6wtVZE6mf$r=NqDrT2~TkQ#NPApfik^yyry(~6l2=?Qb`r^yP(HN zY(^1HFqRqpseR}t{t@ZXb02;!hDHQAa|RM|psr+5JxY)>iW7#~e2cIu<-)QiwJp12-LEO`Ik@%m@F^-a2`!TDH$SrogWJSQ81%I(pXsj*f( zi7K*8g}s0O!*CA&2RTe6&v~hB(NCXvB%ZtiC-g|8=osuaR=B;7JX1u!I)Ty|yg|(F zy5n44N{=#m3Px1X6&7=^xiNwPOPDmyuDD3%`j6ZU2wKz#F~Y(Rqs%dP^aA$NLQSk| z;2e+#dY%iqsD6|;l08g^p&pyTgMD^ShkgP#+a75>yBwSbr~zb2|CjA3DyUmu^m!+J zVxFgkOPM#RE$Rv47X9(!2uxGs*wAj^8!#U~=f>;DQ+6=TK%j8$BE3jT=pFeV?vP;j zV_Uf%vigqF-hbXEN6*834S#XLhX0uR{huu*|7+#>uZ8E|yEy+l=%r@mt}TM$OSW#z zwz*hRL<6r`N0V>e3IkP)nlZO03#;-!u5*B z$oCaka8Yj8r0quCIPvntsKp38tD%{1hlw8f^7cI3?b$_)Z3HAEq4PE5ixq%vX-8zJVM_F}6tstpXh2$9Z@cQ5v8g*;a~S`zx!2CfYh8Q3RM zPat!}FHQ^PKW6QdM)~7xm7WzvU9(3Q(n(sYlwIWX#W5`BEUW7`h1p!3`vUfK#m4QA$DV zbAAYiJc4_^-OwhcI<(W`;E0acc5fXo{*av@Y7eKV@qO zg_LD4G;TVNCOMYRpT?wtIPyXs8(raQ@~NK>r(+C8tBl#?7n9wtat4r~>pMXxnglMR zblrI5tG!5?EsgDE)i;j0eBdx;uz{eeVO_9jgkYWDN{9>I^H31csZhMuYwT4y`jzJ3 zv}9++-`q;1fPq-N$iiI0XZzEvU5K&yI5}pqJmsn7`FEAmCZnd|WEA9T*XRwhTBCux zA9YXy;S$5lTQ?x)&-77ZU1LE)=H}^i_*(670Z6D&N#m zR=CrMtWJq0nD*Q7X+|kS@NZ0;d|y$Rq`LCez^n7|1TpMaKt}nJZJKoAxXw$a@|=&# zo~RUOT8JTPQF5Y7SjkxfG)p~MgMcDk7L$cA^w&WTtevqirQDz%D?F`P-GocU0G{Cl zrH(sV&D)Qpb+M<+Sm=HHaLSK7(ZWF0XDVQ5a;AY3f^EV**zgd9U_P`d_Stb+BuZ6$ zX2v1t% zlC^S&+L3%_8~Fg9k;QfL?j(L=g|QpuN}~&fVPjz&9s^{PmO&U5^@(Pq?AFRd0U;zq zZ>OxDu=jTQp@oNrppe*(kry-d#qhuR_f$>?h^cS z2n5|ON#l;69;~>-wm`}2Ccf&UJ_vSCa64gaCgu@7aqLbSw__p(S7)JBft%hBBf7(& z*rD0kUTWTMI)e%r|z#&u`W z&UMi6M*}8z@YJJ!;n8F8prU#aufI0L8g372s7sY_yLw+L?0(M4#!ebuZ$$0u6;eq5 z!Wrhno-Xvk^5CL`n7P++eJ$+f2~!OfhH5yvT?lZybl(?S|H3}XTUCyMoK>33NVCY2 z^re(rjS*gw#U)Vfg_OjWb;O-Pf&-N1b0RDue{Fw_15PT>#B)(a^k>fjnrj{W<MF|gJaY7arM>i&?yA4*Pe z6d!l^knz1o&1c$DO{Dw-j`db%k}2K)+EdRQI`TO^G+-!w!$!(|Jc%DpE?|Bns+ z%q{aLE}O!sxtrjG-_ji!#p)>IiRWU1jNkm7+VT-jo$qJ#Y>vc(j;kF5%Yp&sy7QKU z^=Pcf7I3%gyaWbLiS*!OWiP`_B6^QVk;)|6k93dcg}mIjTsbJfL_eel=y5vA1NB5B z)sYhUYY;TCO_FVX3d^S+ zGH;mEKqx#Wd)OTSH56MjF28UxT96goo}h|PHzgfpB#=6e?#O{|Ge6fPy#>IwvK;W3!?MP-$YvdIsy;_S+b^y4MJWj4{FGtLDiGys#fotvHC`FX36a;+LqwPTk9to% zk8;oDHl2R+o!^L2aF+;`j2w(E5BkgFSnzZ*x|8#Yy`JNYjN^=Dnzeh_-2oy#=^{Lw zOOU)CGDdoW`w!~MW%UX5ScCfx#CXD~eGLvzGAc7=zip|7*bZPw&ioI5?%3D8@!< zxb?YeS7~1A4nyVJ*l9&tkFpAZR*R@AAw<6UK7nO`eN2<)bfzDu>J9nj*H9$*n}_K89X{@X zI2|S|g--4Pd%E!o&VYew3{m!E1}^R>TZg6EeC=t|%wx6rJM3{w$8qT08NZXxb;Jqh zZbabg4SP@TmuPO7*Q8?$e_N<@{IT<#(jR8{p$u@Awg)@g1PcPpx6&idN&4 zDUNT|8gQDEZ$PeMr|3yW<~{H_GXtXEi1ZQBg!X5ajf)AL*P&nkeZsl)Qdf|NFZQ3q zRKxkJ-`;!4Th!Sk(aolOIoR&NJwBUweT!nSU~Hi;hpk%xE?PGMc3i=H@@2X3-_rf3 z5I`^c<37OYGM#jNOw!X*bxA+;w8o;|B}Y87Le{vK zN1QC59)JF6Q#GQEX`B0-Vr_)_pJuH8XOqA`My!9^9{)9}7Axuev#WS8#kRBRw@L)^ zr>G57LqZut3nKW6I5nO@zQnVIxfu4=%)w0xuwU{3kTB8xKbndgEs7G}0W+_5+TCV& zUZ%1;dwu=50PfHmN#D_PD0UEI!1*L=F(8k<|0J7BEF=39sSpDW^PJKQryQ$#FF{hsK{$(MN$fOl7lHgvLl3=H&7FTiO5Ub4C zHHJoOFACLaKRRR+vQK;cVL+_OdSjSCFdg{&mhE%fC&cgtLao(Rn23_*8Jy-^H@$vf z`30Ja2UDpn%oNB2>qTDQeL<%C?4F+bx#Ib@ZGKv_W)~LoAbDwe=DCWgFcJJs@d~t{ zpJ=|ww9QfLz6?iR^%>gg?UO8nNW|#U24y^@Y+Qc9hK~GLj60#HfNRDznKM}_BexiX z@vM~jWc0}!z*xp6)XB5XoJ?S4;MczO!^-&T?37!b=ShM?P`{9|;i)B|tLVF5oEbL$ z;R+HMx&s`;dotrYy{KL!br_%6vZI4??-QEytC>XfiVwV+bd->bB)5WX^ztk$Isux?P0(xmKR_T=s^s~BS0fwKNO|zAX8|+r40qbI##qQM!sk|0z@!A z2qhAD(0>`*YRM>iUYr~unq{;=XZsl^+W-i+KdR8MzAr+(UcS=QppU8y+tcF}h= zQA>4+uDV*Q)%j0Wh)3e`!T8O=xKAWmt0)&|LHxK&|Le&5_>d@70(dhhQogydF7Z0S zSZ9&$UN#2dCZ>Rq#$er2S+ppjYUP0xSuDeC#|aB!j3tk0F3k$_D|u)qy)Q1muBgL63~QVv5e415AF{a?+K$-Ea!%YAB6> zf>09+x-M>1@RFO^T;dvf1QhL5{u2F`9?e~A2P70N`Z+F0Fb=C-b8A7r7E+X5D`Ip1 zj4-3=G>m4EtpqTV#7%yMwIpo4O}{X`lIw|P@po5^YNb*+BAb0go~gO4YK9|zTA)E{ zxc{R0QsH6!P=uReS91T64@V#+R^t2Eiaz@iYicLUov-NlJPjT^z6B)S4DylPtvL(5 z2>m0tDsE7sv=$Y#T3XqVvSE6Sv-pCnb|%OS22u0AKDVw9v~Lc7d?UokjXQY`$MN90 zXXxKtpMTnNffhz?WQ>Ir$f`*)ckCzok@RtBf@g^P^%+o8}$ zh=t!s)DDO*GN6JsDyeKGgJ+UvH5ZPV0!Sdb9xEkn4;RokBP7Z><*W=yjcGa?*H?@& z3@qkU5qE+}Xa=l0=#89%{i>B5Wz-|W1nOyCFG4ZK@kc^5LXC2EBpx~8Z|eBy)erol zGvhel9E~xCT6Ts6s1Tyna*odp>gKLy;`W{`SC()hRl@q~3GWvQ=0k=+G6CmANOlDn zXiy{eeAE4|k&h4|d3ohky?NT3lL>`5yysN|GA;V){>2=JFJs=))T5AOP@7DRhfolt zSnmN6&OmZo{9eo(xGjL!S&&cDRy3;V>d{ixa4RH;+2?DsbeW2^C#xB4%Kr9UOu4Ar z=EH}~afIzt!tj?Mqn*TB398%zOb7`kf!s?WFrer)N}jR6o=zSJA} ztc30r)T^+Up#z?p*+x!>^JIh4x))Q@z?Cr86A-J~<)i%S@>R{?B0$fP+d={H8z-Wa zNK%!Gb{1}Axv_Xf&3$N2EkoR{)}#g~ix|>#N`ks1FdE8ACPF_Kp_UvdbFWwF>32uI zT5N2fTrBziwt`l9RxsK#idMgXN@TKyx3bI^Axqd1vZaFt{MYN%>pBUL<&34gt413a zX0W9-6L}nTWt_71#n5=I4=}Z*uxuhi&I%(LnKZ9Q^s7XIWs92e8ZOG4Hk@6TyRt2f z99CC=S_XY!wOslkT%DKQ=o@qbDLe(Zm6ko?`-S5Ww&EUH$i$D1EQ`N|9H&K+Iq(mF1&nVcI5#N*&l(AX;Foy|SjbOq$_d%4Wj+ zXOs?fy=VGpNpl5M+3r1DdR`F~v;yE>kd`kKu~^Akpc?@2^>36SyDOf99$2n$Dp{A=k^Crh*yYf*U&4=P_X_L`~~Q`8Np z*0#+_SVcFLPt&NDH+5|By;3KQ-0)!0*XY>&f~cjrD`u>TFs+~%a6azRWIONMHb#(0 zbCgk4ikW0ALB{L5(-SwvZw8f!Wk#yg2z@q&m;%L99Tc-Md9g_?;s#L8ok(-kii~g$ z$%j)!*|eq%hcyJln=_Y_!$2~#ub6#|>qS5YIKPfUH#*XUH58C9f=ZR)nLoLNW@{X_ zP|0ngNuEJ_MinP$urLGnVdm9Cms_kQ0=>#8_Zn@ASSTe`P8wd=C%lxrh=RKj^btlz)9~4{5#HsfuW6BSD;)j=QHCOPGjlt?@~J zDuEP{;cnCne@zbk9N?7j!GQFCOoSX=Zp_BMC?{N@q@^DoP3Y!UXZ8Md5RNRag?y_L zdqyVKDAG;}g1qY(yR47N!-+OZ+__Y^OO zqy=mgR^nS9*4J#qTh2z=VHiy~Fs9(^r0Ps>{^SOF7?Z+#vuEo5Xpnd-N@KeTZ&K$b z-MPmiWg18Y&**S#9|5;)D^j*O^GjlYKN#J191|;` zz|zT*awg6|NJwWVG}^$qhAgNF1lsL3aeOjL$Qmt^ls@WDB?QK-H8+40MUIzr>CNaD zD7DVA0xzl&3sKtDQZt!EBsF3lPBghJME8bLxVIA|pED|3#m}puh9r?6?{0M;#82~^ zpO7s}FqZt>lnuV?CskxQ22F!MRMECRp4XCZGiR^UkpT(Op0!REV;RlcxDr_+;==U1 z8}6w3d^vq9=$KuKbhMi7Q!&Y;ldy2Nw^R(8--E7Te95&ti@zTc3TX5xRvrQjms9QO zf}83RZlHxQipeiYaLsgdv0y|x#COzpERZ>Ac$-9(EwSt-fhdZ3ufgd4l&R(y(<(1y z-yh}QuD=V;lR-VlFUNn^3rdKgp&iZ1AC8cayK1bw85T~!FEPGSnNJ1fRCmsBlRKE<4g z=T;ecHtLimL1r}QYF9rkq&9M%P;Rt+O0bYzlW!ktp zpCy`FQQ){g8ei^$Uz+IH7H`^km6TAqhq+*`ZKpUJY+*rU?jLeet%;xf^3=+)eL)ub zUPX>0LHacQEPeKsm^opYQ9xJflb~-O@`(r})F@@kcx>vjzSrDB9B6>KR%Z?Q9y?+W z%}`QH-snL8bygvIswPC~3Ff4}8_N;+c$2-H zX8=Nl03BT2^3>1ERm$VYDEBi%@&Y+?&Ll&R9pwW3=`W?D4NPRC;?OOcITbgn@X0W) z%4dx>;nT1g_u-_fH`6^1Z~b%}eiaXPHeezXnSoksKA|&=osPrwNV{HH!9m(k>G&no zPw$SSBiBl|-p5V`CY%h_rD%%Hl*`xe1p@^*yan{}O7Tc7AMqRN6K%2Me zdXhHUulCzosPX&Kl22)0*_a5?;GrlZ!xt$f-{Lioj5bM_&yl3v3jcr;d-@@uBRB5E zkAQNDjRfoMvQ|zy)I$d7TuGIAp5oUgh0u@Rl12oaEKsLh4L}#TC?EX_r zN))n3k}DzX>A6T8C!t`8Z70ssD+e>n7r%m!Ma+Z&pL-6LDPANthGcKWCyLB@rB1_P8H)B_Xz@L>pib5%m6tX?mq~j&~ z&(Wt}g0XFe)S2hurpxmFDrW;e&2xcL3ehIa4~|Rr-ngoU0IKIO%kn&wr|-?UI8P;K zrQiie>0%qQ*88lZj#3OWp6m&s?1w{oZKA|V|N>Fg@65Ap$U%_h*y>kIAj|#mX%JhC=*Sf!c zd5W!(HS@tPVp&ew;SIr5{@E{~lq)~QcderQ`>tV%cO!0a3Tbx6u^E%L(S!`FNL`@If7 zcuB+SIc2U!020Si@-*>H^9r2ToX%Z)PA^~R&+F>;JrDbM`g;h^v-L;_eq^3qE}P%dcG3)t`!Dor@VY zdOIUL?gF<5He0i$++R;J-jSV@L%H{a-6sUnDocmYGd~-1N_xx#@yfPTz^VG?jI@bzMYNWC?%mJqkvv*aA}$fC)6%z zo{R6qW)EKd7lNJK6A0lH|`mOm8B*>Zs?KII^9|Fj=FXII%Bgt*F& za+U37N1bM;1$3q@-V7ODExgy(yEsnwjIexQu3lMTH=Y`ykhRDMbVQ(z7uHMF@jna? zc4zl}=4l(qj{sA^)3Jzm4wSHo_sZk~cyud|lor10K9hOdGTdeO>&XwKbEiTL->x}8 zd-M_(VoPmk{K3YFb5`eAFEM`N-{p?m$I{ziJM!>`&3PPW(tE6mo1o^#`-`P|*L45m zvmxoBIkSfCm_adWG)VvsI_aT4NzrQ2Fd^|VeEKS~Bu6VGHI+HbK;l6cPsVyu(PaUS zSsViSU`Rok;t+KU#K;RrRWwl=kOR&PI~L@vjFN;}bTmY{E0EqKNy6?sEcx6b+FiX# zR$2ip9yjq3c33f>IBS6D!K@)UvlUpb;>*I?VOO>EQ-q0;Dq*7RQ7l%R>Wv$Ql_tsh z=)|NH+%VJq+NI3?+LPoDcSW`hFvBgX$0w`BBhZO+5K-zO$`h!GJ;KQ?#XFn02zB&m zlfzFJzZuyUF}wQkCQ2Hn_390ldplg&bp28v8g7v#Yw2skvI^#~5_!f{Tt(DXi6kPC`VqHu zg*0%A2xj37J6tB`OHt=|{+4??hk`Bz>H`w3R;SusqdLjS)U|$z65Bh)i6XsJVRcN% zQ9@|AkvuLZ!mwO!yuh%XorFf1X1{IAiSFLG$T&GgC_+lfQ6pInoDZ8)(juRCzbyi1TBloXGk|u{Yi_ox z*7(h*`;@`8bvrPj{9*~$Rpi~=B@p8r@91kGZIsFLhSWa$?6={YB-!56Vr`Bke`6Gce=?w z5hJKx^5GiX>pk&fpTR>3^itW5?FWU9r{kw%yI?X`Qt1U# z@dw}YeTLljP8?OZGhE|`+fkr5WnNCF%k#;xKVCxRk)`K1=AS^k1URJSRfBwOqSXiG zcC6zEgqe4d<5u?gn{jqHa)Bh^;3v5L(itEaZi~6Fu6|K`^T>4WvfX}*kgA*FnG=0N z6^sc>A!;E*;CIs&>6$kUsb|?wucY4Eh$zV>r zHAw>P5C>)6Wqo_+?5X6s+H_~T24u=qTn%|PcmC-_R)&PR*kQXtnoo=xv^MTg?`y6$nK^11AJQ{p=8b)jnEu)7&G z1D#U2Bj3!~@AyC3urnFkuy5{lYx=jYjc4dsXczZuv7mQ-g@XpZ_%VzI-|~lav%|jK z1dY^CvfCzaHW;Oz4^={in#037ht#@E zRdKQ8xUyPmGpqhN`!0ixbT8XrFEcy`fItGn8R|zxcHk?O0$d~xe>A2t9b=tJpj{GY zGj-~U>9NRz%P>E6=#ZnB8&>EbvR&X+Qp7x3bYv5YSj(f7Q&20lnR27_N=b~w?!05( zDvuR0Nu3?z5n4O025^0|w(W$bAEC90(uqzE-LJl~;3V%<<)O~4T_c0ra7`avAhJot z?0|l7*f{pwI2PSFCfztj%_87R!?zpOc-rQ0PupLcy9n`Dtd1hItj%(XgIasUO zz-mpI+aagusk`95rWDBxkc%hw9HHT#oxxZ`r58n*=1q?E%kS)n2OJT_oAp@#?3dzMsZ(Sl31&U8tl( zLzgInnLZ@nqyz2YL9;I#hnZEe%3Jres6jSCZ6)FRS>sFxvV5Z$tU_&d=Z8Xk-;kUs+Se>~!WhU;%(P z@B&#<{o098bW{mKYJXyLM1_Ek&2;RL5ZRZxZ6aYcXEbGJHT!Kjx8?~hyRJS;m}2!E zOOHdnd)L;9*wd_=^d_O-Sd|(A=BNB^<0Jqai*q9`ixal|5*ZKpOr!zVh|P>n;`fO|s+U z8Lh9Os;V@O+E!(*2SL!-wTf<-E`o+*ef+ZeqcYjo6-LWCEz&C!CKw@+H8?b#xOF(K zs6_>+nyxYpI?gO0t}PdwYAz;8ke%B4o6}Z24eE!SA^ff>fq-9yrB`TIZ5bxJRI35} zqmiE+rPeSPreiNU7G6MrI0Az|-}5PB3+0W3g~M$=!#ycHjf3A+Q$l;PNZV%!>I@g| zj$N#5M1d4uBP80hWqK{!g|2&LKjZqWoaaG+USdCZfBkAZ5M-7)Vm=VY5k89%6QW2| zb1M9FHw<;@Z%x{KNLqC{{M4a+4dY|9xwws$5XW1%qfQ0goHO%)jAu*^csFVe?<#gw zjg+!4I4aZP)&}MGHqP1eo&5Y!aSgX5o~Yy~T2)N}#YP3D{-c^MQNxEDy2BZq7_Vk3 zw3yTE3Zt6MMbZ5IFBhDs^N(=i_4sWBg zSB}Ce#uSj`j3PAmy?tz5h&;8!86;b_INLDtTw?5UiV zNg0YgQIN>?@3JHsvFY%InXTtGQugRM6sznfHM8^S@1giNOJQ&ufaOC&vg57(i6x~iYrd5Li+C=! z)@Ax@5YyjKIVQc+zZY$%l`-z6It3pY=ndz?aF^2GrkRrd#GrfJ!H?8CNniB$FyHAm zPGZMGog*JTT9$DF5|W){hh=e-Y61NoQ7XasosjcnC7^+8zl2k_U*a|oG?BRJI&|p5 zcePCp^7R+d@_%dhMBHOWc|jc(JVNh6%= zRcF`_Rja?H`-wtTZ`suCK65dUtNw5L2Wy{ZmC+=pkXRJw0{u2hP_rhLF)*7WNsz?Ej zxPcoR`va%UuThKDk(Sb?8%*3blmmhX3dF-YQEePAlCX*wD(2Zk%DJy$ZQ9>MZsCum6Tm|bw@%aR1mM3iV2 zjA3=aMl%2NHIm5_w?OI>35W^aJBYhTKjW4#XL0fUa5=)T}0!$AVn|| zy!?`#wXFiWJ?iu7(~6uyZU;7pZxKj#DGalOoQ(iCqDLgMO&D;X7J%J8Dd{Ia`fn(g z_n#c@13?{9-4vT!mn<{K+KQuZ`j%&_rpU^#ir(Lmhf}8uhJ;ou{jC`9jIr5N$`@SR zo;xOUy%)fe4EjX=EKS{0_LJ`1I{7wOG@JKmp2p1UvSE023w<7ezieJRb<6zrayv{l z%2FqK9X%-Bz;EkW*1j`sxHf-|9{B!=Hsq`S?7IAAVi#fm(?jlmm}&A~yTSj)8va|R zN%6mW?s$|WTeVhD`wI&6mjP+aSD_*_j{p!4OuB~>cwbwZfS_&79Oy7V)iTP$;l6+Q zCEaIU+fytRjaWDAPPIDCyxBJP`u@B^=p#&mDnh%QyS7aax(!%`YH#Fvk8=sOc<4i6 z-1B$je5H8PA?{**^PU(y4JF&ef`KkT7k1u&t^Uz&x9Q#3I<6k$4>6dBYq8mR7}6cP z*1;9|V6|zBvUZbO%P83j)1tu*i2)~~MbUGWKkA_BV&4-R+sYeb$z4d82T14#@6bax z%@#nvfKE3d5NR5qS1xD8Sg`WVJf#n6M4iE(mA4N%qC1=+g=5{4iDio7@SS|lu*F}} z$1%2p~hdPm#uX;t1 zN+ywafDMID_~NRQykU-Gta+6ws0hP@{i7KzY-L1t$5-)(cv)Y)jvkOIGE>4dqn(Ti z((;nOZY`ApLUNW<$fYl@Vw(Dfv;xypkY)AmFmHG@7_+#%cUnvaWJsSUnfb~p2P=e| z`R;cUiWMCpyx{?InjY`B+nb%{W}89Jykf>K4u)1t&;1}%6uR8!+PW5eL%F2j2| zDn-;hS0XzUdTHx2&u=I4gkrOPGMIqy9x;A>&FPLJY0Bnu9d1{zOLJAi+OT=c( z`61H3hU!R^8Z1(EE4T(X_^+BN+iIlxTV#cB4D@-TLdyFY-h3yI6Uy4n#W+(}e6PoEhQ zRG%C;EJUsAqWkpn`*bdLXrylk9_{SUV z|2Zuc|3CgXOA}{jkN*&rO8YFutKM{xLHItL|HPd?9 z(EH>21ELQIg=mSc9C;c<`mkxrnuwP2r@uvbripQ2xh3fI+7c?~w8&ab9GpqG^_8o} zq`f(xN6rpn(5Nhvk!CT|6ukVNCB%MW1{{MxTV6hrGlX=zX-JVE74=3cX9hAP%@&Kh zb3|uxH#OS8ZgGl<`04RMf>C$@%JUZO@NCN zy~t&CCO+BV2)G_GnzA&}H;xu$yGP*1!s%@S zff#*F3?mTz&!%lq{&Q1P^&4(R07I10q2nlm>PeZf858@MEifC!3K`ycO2B5Dkk8pV z30?^5Gx81NjXy2DhqbTd53=+=F9CYXvBP2k#MyVQjAddc)q#zGdWiUl-UmM%GWQTs zg$ld zj4>+AGiWrRG?IqbyZL-Ob6na2rYEF)LfYGG{wUC}ZjxQbg4*YO%Qrth&@*B5Rr_w~ zdnS+71nJMdy-Ia&JRxL#X4k+JLQ>K}q3nIa8()FH*u04JrI1%Y%U2nLAmGNCh zPHY~9K8>6Jmr;DK3l10>F%1I!RWqUqKHGYP+QMKeo(1p^ScR}oA3fgI&HX=9tIh1K zLC^j!FSGw~dHK&x;s3e=C7tYS|N9J?tR!Rix0TEXi%TYh)28Pg0Tc3=$wBJR08CI^ z9J~lXQPI&2G1V%82AWqO@J}cfSX@{f`yJ4$LfE>DxnLco!~IOut{2ne%J)CrSuYM* z0ug_mBS!rqpQPw8h6Hb+@(~XBV1x%k`Ac5k+^{BEL#O#sBA~2724~%muVab4e#=-9 zJ%)9mz=mbb@B3*V24}IrFXhH#OmjdXTj`3YILGA@Q+=}Ntj_zvVMeoP(8Vi)ZG{E= z?-@wptw;!!t2P66Gw_P!vK-wyUg%@|JcjPty;aZSy%T;^EFDdQiT3!8b@I+}V&VN2 zY#o>SW$fZcGGbh?jAN$tqrbBC9L~nh%TDxxU za_i0CYoM60wT!T7ZQ>G9IsLear(JQr(dq4Z@4P&+@zyys?_Q7m*poXHogt|0dN z4QN{s#wlrDAl?2=CC#Nh=+Yk3)tSV@mGA;79;ZS>Xob^P0$JCeE(++89ecZ zF8<&DpZtG>|2X~!Yq;b;o&*2gclj?VW7-?SJ?Ss<$Kgi$C`(GGHNg5Co^>2F5Go9! z000m;0Ui-N019?oijh4%oC%55O;{`SqSR&Z;&j1s>jIDy;Emd~F{Q;)ORMr)t+Um& zep&T$=7sCU$Ba<}V&U}dXWw9l>!tf;%j<^Y)GdX}^K8+qM^u=s@*0$)B$2RRl}dkj z7A`pRK$Wz7J;Ds!co42nJP!}us--(&Fi{?J10Fos-y!WlR|=M6FERYm7t%Y z`*)53s~WK{qp{_vV!LqjuQ9;q@fIT2-N`ZUZ-@sk~l9I-x$ZkxF5)~)GT&bL9 zwLtbD2&<4Es<5#8i=U~2v4zkem^U$?+k%e>K5U;qO}VMR`U=>2z^y4k=vcQHik)FX z7yl^BS$f0=*BWXSE|Z30vYjE941a9~C1YZoF~&T!$jdM*AjAt$d1E{v0Jl%4IbB@{86=L zH^P4M%!t}VUBiSMI`AJcpMtCk4@PYb8A1YCAT27gP z@BuHVv;2j~KIG)TrIV;?y9LdY^gPQ%A&v#0j%7l=xD?P>;Thu;29{^*&*GA7!i&zI zD1mANC~wz*7)`?yG~(-oL6XQk4KvZ;#|WkAhNsbzmv z60L9gT_1Nh50V7$j!Tr+HB6is@@kHCO>fRs#1j@aQUpa2@CZG8iPVoSt6rjKy+FbY zDvFTGOQ$o!>>xo8jF*@jc7irUy`9@@{d}?Eommj;x1Fb0#3OJu9skH8go_-A#lKpYQ@r<-%J&z9X9F%FNNb5{h zn4vlbr-mFoNe0G|s&x?u^g86(dDSwpN7!vZKVw)bhO&(M3dS1r>qVdG_szs@Vdoic z;!W|#9(DciYb!C=#0nxtjCYj8o7DUQ08Zl6$4mNi0Z(wv6;kE&3Q&{{oZ)hA!-c{p z6<~_{IanzovEr(aHVL`8#4zZB_#~X=RyI~m2Vh35cSLEnhd)6UGteAn4lQDZY+f>+ z5ji(4J?N<;4Fa{dQ?JyQZW&FneLgn9r=X zyQry`AO{q9#q`jqA+%5lIoVS3{dTV ziMh2_CfL&TSc{iW)#k!)S;Ni5It$ez^$oiM_sB& zqCYvIE6)l@9U`)J)298Q{8}htJ5y@`7^ir~L3?hN6Uu&ek-&_6Fq4j*9oseT zEC$qaFP%=?VJ?(U`m`hM7V!f+GFuG62o|6kZtTF95%eR36H9hUC2TH1S}7E zi9#dDOLGc4mmCi;f|*3OWT~itOO0_8r9Kvp#lwF66(A(D$&Y+6=0Hj}aOy5=>1BUfzz9cX)W3F^({ZE*K?Rh}=3nmFD1xV@i zeN%}A(+*#(gFomf8QHJaXj5k19F(TRdzCG`(xp{<&s9)tmGLeDddHqU?sH7pfIM9*H znle@w6^suGCx4SjPY-SCDqSLw95bZJP@$1;*fuHL@SS8eSkdg``-7z$V{A*thqVFq zQ^7lPEdI>XTHjHZB8PpyaPAFCFL-*5xo`F|1@u9ISOjML$gXStKqz=}uBlnC zd8V4CER(t4=TSaBNz9BMuq=|Rv=8v;n5brtvNQB-n*ESz-?(#9GIEs6s9yzrrmNmK zI+3SBx;l=}R09OeLFiE*p=M;DOGMzcTR$w-wQ|H{Vzm>ew4Y*eDZ?wpt%mz2Q}x^u z%dpEEmcEHJW9&9XX0Y`hG;#J2KMvj<8+z1aomYFsla9@>^0+zqEmFu~I#A7fFpbt| za@PC2uziapJIk&WUG;y&Du&#rTaa zbcM7%J9H=T1}$Mz2s`_$I)krkTJHtD3?Fp4@RLueTD3#Y!1H6+6?llsS&yP7+YWW7 z`V2ksWa_bS&&UsrvLl&z80ImFZ`C+243WXq9AbNcsd+WYirfYd3*AwLnl29Y%Xt%= zyPUhqDx>Ga86LN}$QI8?IIq1P9E)SFMKh7?Y$}TmMr9QzjS+5Mx{Wh&3XlHdFm()l zckQTgL$$JJ-;QfV?MG>Un6E)eB1-%%Y>f{y8L{C)MQjzBFr}1KOwHtkWDT9{x-UJwd{?(rDS(yL|I#Q0nir88XS!B(&ZukH`a8IJ>b5XwR$lBuMKs9D z7l@P2s$%l(Bk08Q^;z1uo%wNA(jbzOdUYJjt8px?#>FqKkv8L@azt6jm?uKr=x3YK zi5};nU^30q=`E*Ghw_X9a^eOAI(cl)mi z+*v>2{gent#zJxYk)QGUkq=3-o5|WV+ayxo7%mHJ>Tyq6VIx%+mJAc@D$UGB zp=dmlj2UE^C^X(GvSh^RzJTV${OQF@+d|Cm-mb&#{*X62Sj+eMpBI}NJ;0Tb+=-N) z8#j4&z0D>vgkp)}Jdal+>z*mG$3!+C;4xv2+}GZN6ty0*KFJ-nt!EXMQ0_M z)oH67^oRycueyI`4vXMnBTJWL!_C1Wn%Oh?ou_p1bSd@e6%uL+9EC|0&ni=K&rIjOri)JIgF3#M{pl*D%kD-- zd=DVTeoy;uO$_bXKk{I``zE73!y&zIh`#+J3LUj=Qigok11@J?EUSW*`6Bd zfE*HmAGKwTM+RxW6ZS%^QsDD~XXc&rx;N{+1Og%e#Ou^%Ojs@@_c=Y}Wke4W~# z*y3~Ju3T@!!}mHhl~rEJ>`ybVGn(DF$cZ>Z6(J8u(<=;vNy79?A1|90bf5gA$6N}lJAwN<1g za=K1HOZ2B?T#n8&vJL_ReAK5q3Yrvq>tkCX*l0qlQ%s%RLfEr*+2{s*Zl=saQeZD@ zK9#x`#(gJtEUM)%2~r{ucS`iC4vM#wU9DIxEIqChT&+m`lEAELe zr;JTDO^NcM!&f-^N|sd% zr@V&Mi+G#du^PK?V%}nio;FV_qFT~kdM!FmRZS~ZxBJ7zFVkmDQE7fP#k}kIG|>#x z7CC=M6jR2?nm%)~=&VpN(V8M!*)*Ka%bf6$6l+Cmg^%4lrb$Dk=|19-Cd7%4XYCo* zZe5#RmfsqD|77@C>?sJkiMG13%Ak}UWuF2EHFdLn^BP1kPrzb=vem0puH6Bm9=5RX zZN6!H8XnJYVfia%>xbP{#*T7-y;{*8?dLxHzWuM)I}8KI^6I?Ea2?y)LRUZVZzW(f zQm<_dNo~~<`|n+8wOLGCWacy2LoqCUKDvAh;d}Yz+76-hGGjgG(E^KFez$h=RcW(V z0^6>dA5tn#tx~(VW#fD(53Xg9mh%o4;E~#kN!E+P;1bO0p5FX^C2qA&Ne{_Y#$ksG zX}4n1EOgkJalKWeEzObMyW2}RH7?n^GuOdc>!sJFa+<6c+LP;fL}X5%iv*kqID>g8 z-6Ot$AuH@GP+ZRC?Om}e46l&IY=6Fr<-TFWe`YxR&EsO>ux}12Ub^?Zio5+pG%;K5 z*}XXBTOc@U`BZeWjTU93SsZ_woSWEO`_80Npok|nPSpx?zqopvQz=TVJDZLuNBgzH z82S|VRdDbbNlsFokz;AFmk>wWMF{CG;&U$rj*f&WwJnYz@q~}_b4f#U&v;z%hiVL` zt>eYlmm5}b=XSjKA9K6#^HqOzNyHwfJaruG zkr|9wAw<3!dpqweeXON=IJZHp;y?&{2mTW~dx)6O-EsDEW- zhifA#4V3$q)cWo_!>DC!AYR0)0)@|Kc2PO~GDZ z9bS*VrOKS^Go}BTkDi|!YM#5Wy1swB+udZSbb*=&@fzNCdqKE3iC4D3!-h z!6i<#nhoS}{npWm!PBM3vq|QGjZDDL+(40*SFKZ{Dkhf5#1PUH=Fv|=zc^N-S|g>) zd{?)`zv9N2yuEQKO+(km%=yHoESWIvgfTxXk5N9HDD|Q#4RsclkXHHS*JWlU4?C-9 zc+$Z!RF3O6$}W!5V73i9x(;?W#9#z25q~~c5kK6J%|0zjQWqBcE-Kq)Yzf1Id@~8` zthHPl8M=wm+VpwzoX~Y-NLH17{_w1!?TL2tUcaSG@TlZ?}j9lvM9M?~55T!mQjz z62UL&X81C5_2ssJ6o0SJR}Ms>61^lgF&4IVoAt|Em3ilU6%g(>Uh&99>ca~OuvEj^ zdGcsiA2UCf2wOuTboe>umo5r;5eHNLcTN;Z3CL(}xM;(t9zB`morKSa>(f4MdUcB) zZO;8Zjjrg}%K4-ZQ5pAi;eC4qM;#S6xh{WIPh8ciX7)}*UzIRN`06($*Y=&}@($hY zwP7>7a`cVw6rQ2q#fm%1Ab90S$a@@ovWp#~tMGhcmp%#!4i3WyEZSZ042*jzc=>h< z5+f_TwTLU{_lc23$vccB58jTt5-E&%#8-#EMr&npq3&5>4PIWd6{vX=B(q_{>+Ep* z5;$87A)%UKg=$A6zRtU9g_vHo&hE+6mb8pf*E^c`CzbehYBtdvu^5!_Rt$xth2O-@aT!Z0ma)fjyA%a+LhckrP!>U9g9M^esk76L^_TTRo5M@ z!hDxEge`AHnyRq7&VA?GyMt!C$MT)@^DwFx!??h+$Jt*zFvexSb-}AoGd7c_JX81C z%v3PFz<;L|TOje>{i7Af{QIjI z`?lNddIIgQ-(Z!sZTK2Wj1suqUpY@$0okHw@2qM=H(Kz z-fnLZ$v9(3U0Ivgq_xRTkrZKfpYjV0M7H5l{U=uxSIEnX3ni2prw#PZUY@US^hrjz zK1L%iJHC);mlA)9_pGc}y52jNbB4=xNSI$29K(FyvDDY;dr)>|SYPg6``kMsIAgtytgw!IKEpk|*Q}*i z1b&FN{fc8iaIS?(BxmSawTcm6W+&YOlR}Wcea2|@?&B%J?Vf9|yene-h!b7$8AEwg zD@p|Jmqc;kWyqg;6PR|PJteM~#Wg>kcFSUWhp4UtZ-52jo%`imZEF5}c-N|go!gZu zornBMNK*uE7e1R??5#U7?}c%_w~I9*SE$;FPGyAqX|rD9rU#z^xcw#ACYc4x zKkfn3#SN2p)pR%`15}M=RPXhYbT+)`hUJRCmfyemR#T4bD%q*6rMZk*8b_=997B z%V*coy5>Ts45+R!_LD@gS6J3<+=zQIVRWOCSMw~-OXVr%SicX<4V-6zSQx#s`?ps;_%jGUe7pUK z{g!bK`ewtWv=>Qg16k>8F46(U@Zy+v7RUtL1J3k`oekcCreo~|911g8wWOK(-!3NNK&W^6bgmJ!-oEz4QYWn8l??OEk;G;xnRVt5 z7ORN6h|}x5)*~h!-Z4BSr%Vh4)MqB2Pp6NGai2xgLmYY=yWEM(DnFkjH6#5rB_|cT zi%li7LNG03)-12|PG)CNq4<#KWCp{>%^5Y*cB?|wA^Ebl3Te-P9A)Q^;VvX9&K;;T zx^7ZK5(2909Tqb}ZX$f5Z>^yXCqxq^xlLByPK0v+!k8}Mw&NZjqe z=btRwt7C}du^6#R`9l5~KWbKwb6e3hf^M=7J*>qHxni0bg(o>*dT?)8mFm9Mk&Lm9 zjHdrQmnbmn%3YK2n0;sjt%+{htb~wlSEJunCjNH!ID!`Wl!rS(wxDTAOhr)n0;Fy4 zywH_3a@Vc|D)rsRJ2N(rShok#ojj1RExfqQPbnQ;+l7jrrxSLV=9a+(0+W@)>uKRA ze|kIDX-#$Zp{eEe>>Q&0c|z&+*M9A!Iar<3jn8e$#6P0uT!+-N`FVX_?g-HyoubH* zS`DR1;LDlT;UJC8*&toHFn?=P;Et*Lbw%z0U2O=RPI}Ss8IHl6qG9x4%lX!%ZKC;g zLCII&E_P0vz6E_q|KfY9-hLaT5Yz_VhotsqIITpYa>;MNsDsXp-X&p35k_ zBq@)&NB5mQDXG7yOywf65|hHKz|p?o$qT&2uK9bI&~tH6hA|5Y!Od>w;H=!EKleZJ z()e{pV|!OceS3W?V{pCn&!zFW>v9T}f;h3h!ZI1R?+@ego$c!Nar@}Pv~&~MoKcRK z{^k1`r7$JQuj*!3nU+3dew82?whkb#Y_ME*`d0qLHEwlfcK6gfrdM*w_`$-T@Z$0L z8xvV@o~^SYi|P3hW4ZES)nPA6cMXKzC+Fb3YE63;8&M>qu5J+3rFShyi?2PH0ByWR zF=sFMsU2C>0+oo#2Oli-f?LcHOI%gDj`8fn8JkA#OjF1g%{Vb=X_=C<*aVRLl6=te zQRsLWLsR5+{F0bvibWxHT6exj3&kMuha|hTJ{8}&?2ksV=6x@X)%GEqfAf1v^9M2+->hlB@~F7YDcxt?jl>~t1Z$~_ta7yJkJ!r>GICwS*3n&^y#w#M zGm1@%p*E^CF{W#(I6y3M9@U{ZI-Mt=zHx?C$s$_Z{CeP5!tPrZyro^VqZLVCcMRi^ zBMYB}pEL1`uzFUqs;+&5@>A!FY#XRP`^aAvnSn!*W1*ir9Qm=^mA`)K;9z5DVGKF& zwVPv^jV$=Ze-9J*%NnSd!PvpV%+kog*qXu3#@g7*%-YDXs({u8C%@-aQy3(TMMt~r)`@o ziiPK+ypTQ0=QrtwsCIb8NsS4;yT*#V8J$UT zZ{~X43I`s+z*+8x8G(0+r(fkhCy5tMjapczijK36$jTb84Hq{~(ROcB!1s zFCAEzIsM2b+jh=x(!D)QnB6-%U+IRr?(*r5|ewRDO!|iIl%` zN16R&JvG^~@0uOunB~Kf=o=2~Vffgyg+X@7IOLh}x$0#D7fU;AIr=pxFrHew56!zQ zlkj9uW#Cb3r+*LGwTEwzBOYQK;C;eCRHPtvgN4SpKznBC-DIH0C!ykpi&(|@8^z@> zT|dhgq>n5@NGna9we%-0*=jTh-DL8;N>8@LR7b=e9K?(AMuN{iMvG=ao_FGOsKt;e z95)9XA3jsBx@ClIfR4NVTZ<)z-i_0fTsnh=8}iqd2r%0gpMUIW$yUpHUd33GMcdEm zgIQf_HYyS;_l3LSk%c^RHQmLYuedf*m%264u9%0846wJH-7_-GK950e8}CgcH$so= z0(pdDxyEogZm{4wu5S?gfKa-xeOIl-h*X!4by~)vi%A@kK?!AjH-U-L+Dpc(VaaS) z@3Ja1hPHAMP4c{@ALW66Ke16rAxA90D%%k_g_Z0a=NSv>l=;9(%D_a^{YE*Wr^CZj zU-45Y@A7T@3ruwZ(Y5F8nCtZem&|JKth?WL3Aym_8ee9K+e697Sbvh~#Okza@wf!0 zl{X5VH1&jnWl`r&Y1+4w2HtY^54?qmrxmFk5&!+d)R#&mzMZsmOsh;nWn)RUJs$Bc z2c~bIC0oym#IXC|aVA%d;~;^3GK+X}IfNASxO)j)^q3CXSL=-JwOudzhvib}b~4%< z6#IKPIwQZ99pe%GzBOOpbDwu`H$*(-o!lgDAm;iK727IG&n0g6jo7yrw{K*kSTKdV zl>0M`B6?Ng`!xA)KVhlVefCJ5DTMIsqQe6Bg|F{cBj-bF%A1uHjh(>tGZEhtZb;gr z_y#t36m-?C)*Er@@1kS#E!()|4_7rl@WRuOMslgn=AH1XFkZv{@Q%1wexZ3@wnKwO zT6_B5yN zWYUqg+JW64u`f_Z>1FP&W{eHdlYAbYpE}N4Wi)Dy+aAlcf zF$>gsFs$*Du4Ofi-oKoE^-chjq9`tk0m=lv{bIin5kYJyRtvMJ{buCDmH0qqeB=5;3ocPzal+*eWYCa0%hlu8hSeL0?XT}E+;3hfu>v#*F72{{>!>^J+*)*BGH zYKNZINrA7An+mh3$-AwP+`-)7IECXxEf)8(TBm9n%{}yXtEzT*VZuywqzErLVa}(% z?AhHbxt>j|1Rtv^gU{nsp(9EzS!a|4$*_+2k*= zqI!?1`a<*4o)l@P>BR?v8m{-LyCBdSP(Av*i*|WNfu07T=mp_*B%nYbGe$^@T{ByXRLJJT|yAl~0GwQs(182n!@f zc{TFkW51!^_IA5;Nzui%RPx%xa|s^1p~*c&##*L}q{H-c`gU+uw?i`grWTQ& z7^7X5AXxDdXM5*M@O%U3R*0C*BQduWRPUP;HnNRYmP37#`ZR;6+Ct}PzA&Z!9ib9*~5UIKs41yfVq{1vK z^-&w83SrBH1ZVWp!q#yuKd!sTqHG5-3>W|0TPk_0Uvviq7yAES!A0HP?7uSev5Jlj zw)#0fj!)IqmSN{7ef(B^yv_~4UobU=7a^SK?d+5?wR%`DQ`B4Bz#cPX>a3IbJ>vVF zim!K7&SyOCFXmsxTxD2QSK)K$VTu&KM&0Co>(<1FST2WofxXRf4mg?(<_B25GIz>y ziHR3oy*Rq>tcc)Gc`{7R)CW|x#LLH~a*>GXCOxO6^&RiIr&1|N^H2cS$cMyNX;f>S zr)TvE#v2VzS`Yn{ys1W)jwqB&{{x%NJgnGRle~T0QUEhcoNj;!}^h!heO{G7&KI)X_AXPIC|}#o^6_s zn5}swoU`RAafyuNy+-Wy`b)(xY|yWws9l?zA!3fsW4hB|_cAGnR^Hf7?b+8-(h0_p zGvLzpE6S9wQd+dw#hFqWyS^fn_YHU_J&)Y3PJ7sKuf%I0h}U(IC)=p!0d8#Vp5|4t z0;ej@&?E}T&AH1D@p*_5ahTfTM4i#!&(oragrQkok(ju4v8hCIf~StgP-uRECYBBd zDeGczgu{(WW2|@AZ&1GBzNOX8On^A6JIG~1T4KS@fBuU&j~BjjK!C}J(>L}(p9<#Z z6WvSQZw$I>tU|y01;r@6Uv8$yX=Jw^ML=2~X>3wM$ztcn^?s!w_|DnNgZy)z?xV`6 zm)4`)nS1SdbOh^`9@d=Rqw6C&RG!1Ncq0vtNTYdgpT%D-=;e(Z?6jB{|Iz`e)Ml9z zBi30N8~^ati$R9JrGTHIVslx3iR-F!#3VxPl_7xfKD;TKbF<0s(_%l+3ibYEaf z>fbBDwEH4D%62~Tg_)CM#-J9p(Zr{Z{YHxxq3bJtnR=WyB=jlZC@#H9};OXQnK z3mAUGlx?uPpJnoLQam%NgA->KMeKE>tC}D8hgF^z(H@V}R=D~3+h-Wuj5ZM6X-P(~ zG3;g~Vn^&IubU5;8PToCn$liM3j3JBq`h|*`{M3K&G0s&e-t|X*Z#ZApIC26yLLg| zzMrB~q7Pptp6HT!M51wH z&1XC~`3jl@B;;!yZ1(f$y(j9yu`;Av!5}aRxadc`0!*Wfgik@$Ze$$^#w&1@7>TI)KoDX93Xu(Te=- zCzuC*00)N!TBF~(m%sg(5a!4HfByJD>+?4{mtg2P{gn>H*a6yq?Tga&m5pqK;4UjI zG926<*tEz1L#*IMo6PMc}#UcV_399$N-9R-#)#rb1tyIDa+B4lg3Px~lyCf^@4_-htG9W!o2 z8*3A@TYp#wwvg&9J&=P0P?s%tFA~0~loF|3b7v`@+qJQV_D2pJ20esGVETB2?lRxa?YEkHgTfiPp{Sht3Pe@V4 z)Xe$_&n<^LG2h^fA{wXx;lwiV*ZpwI%u{~Cy&b)Po) zD^m1i;6+Keli4bX}x~soD zbnjFTh}p3EsFyz(%D%Mr`8dk{VI!iO^05e(Hn(npdB@*#Pg%^3%_JZfdtgO82P6Xr zS8-yRKm`sqmc}5G8$;(d_XmG46n_6ypP2(dq(O3qZerYj8dOg@c-bF398UinTTw;~ zpwA0JEv%AzG)_ui-0V0mbgeUdcR(`j1vU$-+UMrX#${-b?~z9 z3km$iuKy1R;`)XV8++HoWp!n7UQZ0j>NcoFVZE-v;2$uKR=5kuea8JSM8W_QX#fZo zM91WBAO|v#m=ib+>DNtze{uo^X{vcf&JF-{!4(|fET#jGis=bKfc07%8$zQ8%#joa z9R-TSJOlqu8EF0{&^&adXa8BX_-CZUV=fd1HF(CL2t*MtP}f5z5ci)P>l4DjZvFk8 z4~p1io;e^f7f=GhW~FL}zX9xL>LX1HI{vh2v1gND&^CXVFQ&`i$RE%;wC?z;(j5{j zW9We@ynrfTSPAH0R|AZ4pbc1yLhw8x4zywUB`Q66p~Klpnm@Od45T&kN8qLNJ|WDn+H9Zx|FRO4 zjRW$518bxQ9;Um0qpxpk_9Gh|MT_hJB{(>%V`+hqY7N^DLPr;Yi7*Yc1{VZg*l?PA zLW+N$)bYDZsW+x%^#BPcgVY17$Y7u2B=B?8`_A>la*h`?Z2JG;cN~nBjkOub2QcX$ zDjqSM#il%97<8bC6M_-=IscQ=+#mceI}z1~=3>@R5 z|1+GFmHw??=Y{;PnRmK*uSx+96L2#T7`)Q3|1;jtviWcZr%2Q_fb+tIfETt384o`x z;PD1Gcj@$S6cD2=$lnq$Y}H4c6y*=C{ptp+Y!CDsNBQP87|_nB<3J(CmexNO%l9X1 zpyuF}7H-@Ef_Nm_@8*CPeL|A^Y^eOefVnj5cXKdL;#v*>)}D$52PY20f@=K#5l>j( z!B`I5g?yw5;#`>0hPG&@KyZNdFr~DA$Fw(tR`I_DiGv>I?N+Gb0ayzLFKpH2mUTk7 z!|m^XrU0GRI4HU+2mc37>&4$d3~j7HAo?Q+p$^z=cms2$0H(kPL+64 zLHDlk1c=ID?dI<+4JqgEJOlvjKzgtpoJH{o0QOlD)i(y(fr9-hJE+#36)&hR0w7fo zbp&B#cN4_eqvtbzX?-8)zj-qUh`u$%;czrz!diSO5VjEjBCOw(m;WQ$kDJP&P3jSF z&`?x?cwaX6%=Tn$_nBEtg&mfod8Hu-@#PK5n>8z z9W%opa~?-S84DUr3j?z|1B_i5MqzrLCxF^l*e_%MnIR6xgBnPeATzW8hYJKRtf^!5 zodECfxN|VLedo}r9|y9V_#>9io&?|zhJM7+!})>QM2ZX!PXv&k29O_Yx)Atu0)GC0 zQ?_xmH$23fj|#1qV00!i`Zr7?P;ImArGCXPs0Ay7r83JSItD0EYsSzf)Rwow3+pnTUylbm zv6-dc7F_E^1JkaAzn5uYOUJ_Oqd=!hj`qus^=Shnz+<|B?qO@e*6;rS@^`l+DfaPw z2ZTigGy^*WrvwlGm#W}Lz&hak$9oIML=iF>aQ-A@xe zY5~p>RQMx+nRXyG*sO_!{0}hy*&MW}`dbh{c(tHC1n`~!JlMFVfpRR~LDQBnwl=oc zKhVAXFLgq>x`{L(QU=t7`-cO?q8*R2FTFzp!BGk9G639v-l6{u$oP-x8|bjLLKiOg z0u#mpH7Bg!e?I}W{bB#eU_w7i^~2+&yXscYU0__FfK$SjWdc~o5BoPDc^inCiP>TE zeo&rb^@A{w0n`*IDD1OB4L`)4RQeQVJ4*psPBHq(i1^?JksI0;3x|tgOQ&V*_XsiC`)Z`fX z$@n;2RtMcaab%Z)5^zQbd`JpLR@D?I!`SCc8Dej&Z}qFc`8#XfD);X~d(v@W=3+3g zhE)Fw3!UTsPm+2HvZ9AelertIyABlV9UwcvLK@KiE9C!HQ2v8G9P-_=8XGk2je{39 zX2gOC$G^DL|Afb|FWh6G?u(&zF#v%Q0D;1$a#B#-p9JbZfTQb$kPT)!;GuBPnn4ss zYsG9Q1KH>2S2Fq!^>7K|pT6iV01yoTB5a2x%ki&>(5@KT9Ru$Zv(`7TG(Mt_0LM7J zwgQ+r08<)<%RSD2#w*Gly4}iN2aA7-(G46lGm9loyw=^?4T9uG0j95_s z77f5;V3=&?{a3KxrW^ieyoYP_gN^<~JOJGRh6-B_2J)W__}2{Xf6^3l*+)7m4P1p` zF-+)hNVk5LNq-~(0TSk`onYi&z{p{j_CKDCp1lpk#?Z#{=n1rU>-Irezbt?bX2i&1 zCmFGdjg94j*|MXAK?*|}L*j3w&A^32_SX8Ae@G)Raq9XzutYozmh|{x2r8V6;C?9h zIo$_7ZgDx{bA*sfqG%mBaBvD>(ntsf%N=~Ob98X~Z}2~YjhP`xO~ z0HX<10UH3yO-}@OpljKOgf8hz{gURs!5+-TV{0|<@_~WngDOk{hRt*4|B8EH;rH*L zhV;QDZJ^LJyzqO};Q&GK1hU=N<1Z=yMUifw%I@j|4!?ms3~MwH=YIr)GI-=#4pfT= zuiz4ak*1K_KVtoA)qXH`bYFPzIuv0TK=aUl1O!&|=RIG)_n`-`n}GESh3J38`z6RD zzi)xc^WX&#uyav*F(-mMycj(yNL_}JAaCs7ApUo+#Qx$TbifDC%cqB-=5_*VQ2T<; zo*eF~2hT!>ZDA@Bjvwit=kNcn+k+d*VW%?qlTJYYsPol-hdH><85Tz4{Rv?X+p7+R zIZCfrU^GEh^*5ORwovaY7P^W0;O' needs unchecked conversion to conform to '@NonNull Set' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\internal\discovery\ZoneMinderDiscoveryService.java:[101] + return serverHandler.getThingByUID(newThingUID) != null ? true : false; + ^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'ThingUID' needs unchecked conversion to conform to '@NonNull ThingUID' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\internal\discovery\ZoneMinderDiscoveryService.java:[129] + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties) + ^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'Map' needs unchecked conversion to conform to '@Nullable Map<@NonNull String,@NonNull Object>' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[507] + Channel channel = this.getThing().getChannel(ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS); + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[508] + Channel chEventCause = this.getThing().getChannel(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE); + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[640] + getLogIdentifier(), thing.getUID(), getBridge().getBridgeUID()); + ^^^^^^^^^^^ +Potential null pointer access: The method getBridge() may return null +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[644] + if (getBridge().getStatus() != ThingStatus.ONLINE) { + ^^^^^^^^^^^ +Potential null pointer access: The method getBridge() may return null +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[645] + msg = String.format("Bridge '%s' is OFFLINE", getBridge().getBridgeUID()); + ^^^^^^^^^^^ +Potential null pointer access: The method getBridge() may return null +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[654] + getLogIdentifier(), getBridge().getBridgeUID()); + ^^^^^^^^^^^ +Potential null pointer access: The method getBridge() may return null +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[802] + updateState(channel.getId(), state); + ^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[806] + getLogIdentifier(), channel.toString(), state.toString(), ex.getMessage()); + ^^^^^ +Potential null pointer access: The variable state may be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[813] + updateState(ZoneMinderConstants.CHANNEL_ONLINE, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[845] + session = null; + ^^^^^^^ +Redundant assignment: The variable session can only be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[920] + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_CAPTURE_DAEMON_STATE)) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[939] + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_ANALYSIS_DAEMON_STATE)) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[961] + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_FRAME_DAEMON_STATE)) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[981] + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_STILL_IMAGE)) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[1044] + if (session != null) { + ^^^^^^^ +Redundant null check: The variable session cannot be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[1124] + updateProperties(properties); + ^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'Map' needs unchecked conversion to conform to '@NonNull Map<@NonNull String,@NonNull String>' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingMonitorHandler.java:[1148] + updateState(channelUID.getId(), state); + ^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\ZoneMinderConstants.java:[42] + public static final ThingTypeUID THING_TYPE_BRIDGE_ZONEMINDER_SERVER = new ThingTypeUID(BINDING_ID, + ^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\ZoneMinderConstants.java:[43] + BRIDGE_ZONEMINDER_SERVER); + ^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\ZoneMinderConstants.java:[89] + public static final ThingTypeUID THING_TYPE_THING_ZONEMINDER_MONITOR = new ThingTypeUID(BINDING_ID, + ^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\ZoneMinderConstants.java:[90] + THING_ZONEMINDER_MONITOR); + ^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\ZoneMinderConstants.java:[128] + public static final ThingTypeUID THING_TYPE_THING_ZONEMINDER_PTZCONTROL = new ThingTypeUID(BINDING_ID, + ^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\ZoneMinderConstants.java:[129] + THING_ZONEMINDER_PTZCONTROL); + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderThingPTZControlHandler.java:[78] + Channel channel = ChannelBuilder.create(new ChannelUID(channelIdString), "String").withDescription(description) + ^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[314] + super.handleConfigurationUpdate(configurationParameters); + ^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type '@NonNull Map' needs unchecked conversion to conform to '@NonNull Map<@NonNull String,@NonNull Object>' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[407] + if ((thingHandler.getZoneMinderId().equals(zoneMinderId)) + ^^^^^^^^^^^^ +Potential null pointer access: The variable thingHandler may be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[495] + } else if (hostLoad.getHttpStatus() != HttpStatus.OK_200) { + ^^^^^^^^ +Potential null pointer access: The variable hostLoad may be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[529] + if (isLinked(channel.getUID().getId())) { + ^^^^^^^^^^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[544] + thingHandler.refreshThing(curPriority); + ^^^^^^^^^^^^ +Potential null pointer access: The variable thingHandler may be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[1057] + curSession = null; + ^^^^^^^^^^ +Redundant assignment: The variable curSession can only be null at this location +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[1476] + updateStatus(newStatus, statusDetail, statusDescription); + ^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'ThingStatusDetail' needs unchecked conversion to conform to '@NonNull ThingStatusDetail' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[1484] + updateStatus(newStatus); + ^^^^^^^^^ +Null type safety (type annotations): The expression of type 'ThingStatus' needs unchecked conversion to conform to '@NonNull ThingStatus' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[1542] + updateState(channel.getId(), state); + ^^^^^^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String' +[WARNING] D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\main\java\org\openhab\binding\zoneminder\handler\ZoneMinderServerBridgeHandler.java:[1874] + updateProperties(properties); + ^^^^^^^^^^ +Null type safety (type annotations): The expression of type 'Map' needs unchecked conversion to conform to '@NonNull Map<@NonNull String,@NonNull String>' +42 problems (42 warnings) +[INFO] +[INFO] --- maven-compiler-plugin:3.6.1:compile (default) @ org.openhab.binding.zoneminder --- +[INFO] Changes detected - recompiling the module! +[INFO] Nothing to compile - all classes are up to date +[INFO] +[INFO] --- maven-scr-plugin:1.24.0:scr (generate-scr-scrdescriptor) @ org.openhab.binding.zoneminder --- +[INFO] +[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ org.openhab.binding.zoneminder --- +[INFO] Using 'UTF-8' encoding to copy filtered resources. +[INFO] skip non existing resourceDirectory D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\src\test\resources +[INFO] +[INFO] --- target-platform-configuration:1.0.0:target-platform (default-target-platform) @ org.openhab.binding.zoneminder --- +[INFO] +[INFO] --- tycho-packaging-plugin:1.0.0:package-plugin (default-package-plugin) @ org.openhab.binding.zoneminder --- +[INFO] Building jar: D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\target\org.openhab.binding.zoneminder-2.3.0-SNAPSHOT.jar +[INFO] +[INFO] --- tycho-p2-plugin:1.0.0:p2-metadata-default (default-p2-metadata-default) @ org.openhab.binding.zoneminder --- +[INFO] +[INFO] --- sat-plugin:0.4.1:checkstyle (default) @ org.openhab.binding.zoneminder --- +[INFO] Adding dependency to checkstyle:0.4.1 +[INFO] Adding dependency to checkstyle:8.1 +[INFO] There is 1 error reported by Checkstyle 8.1 with jar:file:/C:/Users/Martin.KAERVEJ/.m2/repository/org/openhab/tools/sat/sat-plugin/0.4.1/sat-plugin-0.4.1.jar!/rulesets/checkstyle/rules.xml ruleset. +[INFO] +[INFO] --- sat-plugin:0.4.1:pmd (default) @ org.openhab.binding.zoneminder --- +[INFO] Adding dependency to pmd:0.4.1 +[INFO] Adding dependency to pmd-core:5.8.1 +[INFO] Adding dependency to pmd-java:5.8.1 +[INFO] Adding dependency to pmd-javascript:5.8.1 +[INFO] Adding dependency to pmd-jsp:5.8.1 +[INFO] +[INFO] --- sat-plugin:0.4.1:findbugs (default) @ org.openhab.binding.zoneminder --- +[INFO] Adding dependency to findbugs:0.4.1 +[INFO] Adding dependency to bug-pattern:1.2.4 +[INFO] Adding dependency to spotbugs:3.1.0 +[INFO] Fork Value is false + [java] JVM args ignored when same JVM is used. +[INFO] Done FindBugs Analysis.... +[INFO] +[INFO] --- sat-plugin:0.4.1:report (default) @ org.openhab.binding.zoneminder --- +[INFO] Individual report appended to summary report. +[ERROR] Code Analysis Tool has found: + 1 error(s)! + 12 warning(s) + 214 info(s) +[WARNING] .binding.zoneminder\ESH-INF\binding\binding.xml:[3] +There were whitespace characters used for indentation. Please use tab characters instead +[WARNING] .binding.zoneminder\ESH-INF\config\monitor-config.xml:[3] +There were whitespace characters used for indentation. Please use tab characters instead +[WARNING] .binding.zoneminder\ESH-INF\config\server-config.xml:[3] +There were whitespace characters used for indentation. Please use tab characters instead +[WARNING] .binding.zoneminder\ESH-INF\thing\bridge-server.xml:[3] +There were whitespace characters used for indentation. Please use tab characters instead +[WARNING] .binding.zoneminder\ESH-INF\thing\thing-monitor.xml:[3] +There were whitespace characters used for indentation. Please use tab characters instead +[ERROR] .binding.zoneminder\META-INF\MANIFEST.MF:[0] +The jar file lib/javax.ws.rs-api-2.0.1.jar is present in the lib folder but is not present in the MANIFEST.MF file +[WARNING] .binding.zoneminder\OSGI-INF\ZoneMinderHandlerFactory.xml:[3] +There were whitespace characters used for indentation. Please use tab characters instead +[WARNING] .binding.zoneminder\pom.xml:[2] +There were whitespace characters used for indentation. Please use tab characters instead +[WARNING] org.openhab.binding.zoneminder.handler.ZoneMinderServerBridgeHandler.java:[254] +Avoid catching NullPointerException; consider removing the cause of the NPE. +[WARNING] org.openhab.binding.zoneminder.handler.ZoneMinderServerBridgeHandler.java:[547] +Avoid catching NullPointerException; consider removing the cause of the NPE. +[WARNING] org.openhab.binding.zoneminder.handler.ZoneMinderServerBridgeHandler.java:[937] +Avoid catching NullPointerException; consider removing the cause of the NPE. +[WARNING] org.openhab.binding.zoneminder.handler.ZoneMinderServerBridgeHandler.java:[1042] +Avoid catching NullPointerException; consider removing the cause of the NPE. +[WARNING] org.openhab.binding.zoneminder.handler.ZoneMinderServerBridgeHandler.java:[1161] +Avoid catching NullPointerException; consider removing the cause of the NPE. +[INFO] Detailed report can be found at: file:///D:\Development\openHAB\zoneminder\openhab2-zoneminder\git\openhab2-addons\addons\binding\org.openhab.binding.zoneminder\target\code-analysis\report.html +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD FAILURE +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 29.567 s +[INFO] Finished at: 2018-02-03T15:07:41+01:00 +[INFO] Final Memory: 62M/711M +[INFO] ------------------------------------------------------------------------ +[ERROR] Failed to execute goal org.openhab.tools.sat:sat-plugin:0.4.1:report (default) on project org.openhab.binding.zoneminder: +[ERROR] Code Analysis Tool has found 1 error(s)! +[ERROR] Please fix the errors and rerun the build. +[ERROR] -> [Help 1] +[ERROR] +[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. +[ERROR] Re-run Maven using the -X switch to enable full debug logging. +[ERROR] +[ERROR] For more information about the errors and possible solutions, please read the following articles: +[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException diff --git a/addons/binding/org.openhab.binding.zoneminder/pom.xml b/addons/binding/org.openhab.binding.zoneminder/pom.xml index 7ebb2c65ac7a9..e9c7c067ff16c 100644 --- a/addons/binding/org.openhab.binding.zoneminder/pom.xml +++ b/addons/binding/org.openhab.binding.zoneminder/pom.xml @@ -15,3 +15,4 @@ ZoneMinder Binding + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderConstants.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderConstants.java index 471b2b81f64a6..9c538f4564d65 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderConstants.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderConstants.java @@ -8,7 +8,7 @@ */ package org.openhab.binding.zoneminder; -import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.smarthome.core.thing.ThingTypeUID; /** @@ -17,21 +17,28 @@ * * @author Martin S. Eskildsen - Initial contribution */ -@NonNullByDefault public class ZoneMinderConstants { + @NonNull public static final String BINDING_ID = "zoneminder"; // ZoneMinder Server Bridge + @NonNull public static final String BRIDGE_ZONEMINDER_SERVER = "server"; // ZoneMinder Monitor thing + @NonNull public static final String THING_ZONEMINDER_MONITOR = "monitor"; + // ZoneMinder PTZControl thing + @NonNull + public static final String THING_ZONEMINDER_PTZCONTROL = "ptzcontrol"; // ZoneMinder Server displayable name + @NonNull public static final String ZONEMINDER_SERVER_NAME = "ZoneMinder Server"; // ZoneMinder Monitor displayable name + @NonNull public static final String ZONEMINDER_MONITOR_NAME = "ZoneMinder Monitor"; /* @@ -39,10 +46,12 @@ public class ZoneMinderConstants { */ // Thing Type UID for Server + @NonNull public static final ThingTypeUID THING_TYPE_BRIDGE_ZONEMINDER_SERVER = new ThingTypeUID(BINDING_ID, BRIDGE_ZONEMINDER_SERVER); // Shared channel for all bridges / things + @NonNull public static final String CHANNEL_ONLINE = "online"; // Channel Id's for the ZoneMinder Server @@ -50,13 +59,52 @@ public class ZoneMinderConstants { public static final String CHANNEL_SERVER_CPULOAD = "cpu-load"; // Parameters for the ZoneMinder Server - public static final String PARAM_HOSTNAME = "hostname"; - public static final String PARAM_PORT = "port"; - public static final String PARAM_REFRESH_INTERVAL_ = "refresh_interval"; - public static final String PARAM_REFRESH_INTERVAL_DISKUSAGE = "refresh_interval_disk_usage"; + @NonNull + public static final String PARAM_PROTOCOL = "protocol"; + @NonNull + public static final String PARAM_HOST = "host"; + @NonNull + public static final String PARAM_HTTP_PORT = "portHttp"; + @NonNull + public static final String PARAM_TELNET_PORT = "portTelnet"; + + @NonNull + public static final String PARAM_USER = "user"; + @NonNull + public static final String PARAM_URL_PASSWORD = "password"; + + @NonNull + public static final String PARAM_URL_SITE = "urlSite"; + @NonNull + public static final String PARAM_URL_API = "urlApi"; + + @NonNull + public static final String PARAM_REFRESH_DISKUSAGE = "diskUsageRefresh"; + + @NonNull + public static final String PARAM_REFRESH_NORMAL = "refreshNormal"; + @NonNull + public static final String PARAM_REFRESH_LOW = "refreshLow"; + @NonNull + public static final String PARAM_AUTODICOVER = "autodiscover"; + + @NonNull + public static final String CONFIG_VALUE_REFRESH_BATCH = "batch"; + @NonNull + public static final String CONFIG_VALUE_REFRESH_LOW = "low"; + @NonNull + public static final String CONFIG_VALUE_REFRESH_NORMAL = "normal"; + @NonNull + public static final String CONFIG_VALUE_REFRESH_HIGH = "high"; + @NonNull + public static final String CONFIG_VALUE_REFRESH_ALARM = "alarm"; + @NonNull + public static final String CONFIG_VALUE_REFRESH_DISABLED = "disabled"; // Default values for Monitor parameters + @NonNull public static final Integer DEFAULT_HTTP_PORT = 80; + @NonNull public static final Integer DEFAULT_TELNET_PORT = 6802; /* @@ -64,38 +112,76 @@ public class ZoneMinderConstants { */ // Thing Type UID for Monitor + @NonNull public static final ThingTypeUID THING_TYPE_THING_ZONEMINDER_MONITOR = new ThingTypeUID(BINDING_ID, THING_ZONEMINDER_MONITOR); /* * Channel Id's for the ZoneMinder Monitor */ + @NonNull public static final String CHANNEL_MONITOR_ENABLED = "enabled"; + @NonNull public static final String CHANNEL_MONITOR_FORCE_ALARM = "force-alarm"; + @NonNull public static final String CHANNEL_MONITOR_EVENT_STATE = "alarm"; + @NonNull public static final String CHANNEL_MONITOR_EVENT_CAUSE = "event-cause"; + @NonNull public static final String CHANNEL_MONITOR_RECORD_STATE = "recording"; + @NonNull + public static final String CHANNEL_MONITOR_MOTION_EVENT = "motion-event"; + @NonNull public static final String CHANNEL_MONITOR_DETAILED_STATUS = "detailed-status"; + @NonNull public static final String CHANNEL_MONITOR_FUNCTION = "function"; + @NonNull public static final String CHANNEL_MONITOR_CAPTURE_DAEMON_STATE = "capture-daemon"; + @NonNull public static final String CHANNEL_MONITOR_ANALYSIS_DAEMON_STATE = "analysis-daemon"; + @NonNull public static final String CHANNEL_MONITOR_FRAME_DAEMON_STATE = "frame-daemon"; + @NonNull + public static final String CHANNEL_MONITOR_STILL_IMAGE = "image"; + @NonNull + public static final String CHANNEL_MONITOR_VIDEOURL = "videourl"; // Parameters for the ZoneMinder Monitor - public static final String PARAMETER_MONITOR_ID = "monitorId"; - public static final String PARAMETER_MONITOR_TRIGGER_TIMEOUT = "monitorTriggerTimeout"; - public static final String PARAMETER_MONITOR_EVENTTEXT = "monitorEventText"; + @NonNull + public static final String PARAMETER_MONITOR_ID = "id"; + @NonNull + public static final String PARAMETER_MONITOR_TRIGGER_TIMEOUT = "triggerTimeout"; + @NonNull + public static final String PARAMETER_MONITOR_EVENTTEXT = "eventText"; + @NonNull + public static final String PARAMETER_MONITOR_IMAGE_REFRESH = "imageRefresh"; // Default values for Monitor parameters + @NonNull public static final Integer PARAMETER_MONITOR_TRIGGER_TIMEOUT_DEFAULTVALUE = 60; public static final String PARAMETER_MONITOR_EVENTNOTE_DEFAULTVALUE = "openHAB triggered event"; // ZoneMinder Event types + @NonNull public static final String MONITOR_EVENT_NONE = ""; + @NonNull public static final String MONITOR_EVENT_SIGNAL = "Signal"; + @NonNull public static final String MONITOR_EVENT_MOTION = "Motion"; + @NonNull public static final String MONITOR_EVENT_FORCED_WEB = "Forced Web"; + @NonNull public static final String MONITOR_EVENT_OPENHAB = "openHAB"; + // Thing Type UID for PTZ Control + @NonNull + public static final ThingTypeUID THING_TYPE_THING_ZONEMINDER_PTZCONTROL = new ThingTypeUID(BINDING_ID, + THING_ZONEMINDER_PTZCONTROL); + /* + * Dyncamic Channel Id's for the ZoneMinder PTZ Control + */ + @NonNull + public static final String CHANNEL_PTZCONTROL_PRESET = "Presets"; + } diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderProperties.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderProperties.java index 0122921f5dc3e..2e8267b50d25a 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderProperties.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/ZoneMinderProperties.java @@ -14,14 +14,16 @@ */ public class ZoneMinderProperties { public static final String PROPERTY_ID = "Id"; + public static final String PROPERTY_NAME = "Name"; public static final String PROPERTY_SERVER_VERSION = "Version"; public static final String PROPERTY_SERVER_API_VERSION = "API Version"; public static final String PROPERTY_SERVER_USE_API = "API Enabled"; public static final String PROPERTY_SERVER_USE_AUTHENTIFICATION = "Use Authentification"; + public static final String PROPERTY_SERVER_USE_AUTH_HASH = "Allow Auth. Hash"; public static final String PROPERTY_SERVER_TRIGGERS_ENABLED = "Triggers enabled"; + public static final String PROPERTY_SERVER_FRAME_SERVER = "Use Frame Server"; - public static final String PROPERTY_MONITOR_NAME = "Name"; public static final String PROPERTY_MONITOR_SOURCETYPE = "Sourcetype"; public static final String PROPERTY_MONITOR_ANALYSIS_FPS = "Analysis FPS"; diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderHandler.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/IZoneMinderHandler.java similarity index 79% rename from addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderHandler.java rename to addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/IZoneMinderHandler.java index 19cbceea5762c..96b1ac5b7557d 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderHandler.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/IZoneMinderHandler.java @@ -13,15 +13,15 @@ import org.eclipse.smarthome.core.thing.ChannelUID; -import name.eskildsen.zoneminder.IZoneMinderConnectionInfo; +import name.eskildsen.zoneminder.IZoneMinderConnectionHandler; import name.eskildsen.zoneminder.exception.ZoneMinderUrlNotFoundException; /** * Interface for ZoneMinder handlers. * - * @author Martin S. Eskildsen + * @author Martin S. Eskildsen - Initial contribution */ -public interface ZoneMinderHandler { +public interface IZoneMinderHandler { String getZoneMinderId(); @@ -30,11 +30,11 @@ public interface ZoneMinderHandler { */ String getLogIdentifier(); - void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection); + void updateAvaliabilityStatus(IZoneMinderConnectionHandler connection); void updateChannel(ChannelUID channel); - void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionInfo connection) + void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionHandler connection) throws IllegalArgumentException, GeneralSecurityException, IOException, ZoneMinderUrlNotFoundException; void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge); diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderBaseThingHandler.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderBaseThingHandler.java index ce049f2c8798c..01a1afb5463a0 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderBaseThingHandler.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderBaseThingHandler.java @@ -8,13 +8,12 @@ */ package org.openhab.binding.zoneminder.handler; -import java.io.IOException; import java.math.BigDecimal; -import java.security.GeneralSecurityException; import java.util.List; -import java.util.concurrent.locks.Lock; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.thing.Bridge; @@ -30,15 +29,12 @@ import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.core.types.UnDefType; import org.openhab.binding.zoneminder.ZoneMinderConstants; -import org.openhab.binding.zoneminder.internal.DataRefreshPriorityEnum; +import org.openhab.binding.zoneminder.internal.RefreshPriority; import org.openhab.binding.zoneminder.internal.config.ZoneMinderThingConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import name.eskildsen.zoneminder.IZoneMinderConnectionInfo; -import name.eskildsen.zoneminder.IZoneMinderSession; -import name.eskildsen.zoneminder.ZoneMinderFactory; -import name.eskildsen.zoneminder.exception.ZoneMinderUrlNotFoundException; +import name.eskildsen.zoneminder.IZoneMinderConnectionHandler; /** * The {@link ZoneMinderBaseThingHandler} is responsible for handling commands, which are @@ -46,46 +42,43 @@ * * @author Martin S. Eskildsen - Initial contribution */ -public abstract class ZoneMinderBaseThingHandler extends BaseThingHandler implements ZoneMinderHandler { +public abstract class ZoneMinderBaseThingHandler extends BaseThingHandler implements IZoneMinderHandler { + // https://www.eclipse.org/smarthome/documentation/development/bindings/thing-handler.html + private final ReentrantLock lockRefresh = new ReentrantLock(); + private final ReentrantLock lockAlarm = new ReentrantLock(); /** Logger for the Thing. */ private Logger logger = LoggerFactory.getLogger(ZoneMinderBaseThingHandler.class); /** Bridge Handler for the Thing. */ - public ZoneMinderServerBridgeHandler zoneMinderBridgeHandler = null; + public ZoneMinderServerBridgeHandler zoneMinderBridgeHandler; /** This refresh status. */ - private boolean thingRefreshed = false; + private AtomicInteger thingRefresh = new AtomicInteger(1); - /** Unique Id of the thing in zoneminder. */ - private String zoneMinderId; + private long alarmTimeoutTimestamp = 0; - /** ZoneMidner ConnectionInfo */ - private IZoneMinderConnectionInfo zoneMinderConnection = null; - - private Lock lockSession = new ReentrantLock(); - private IZoneMinderSession zoneMinderSession = null; + /** ZoneMinder ConnectionHandler */ + private IZoneMinderConnectionHandler zoneMinderConnection; /** Configuration from openHAB */ protected ZoneMinderThingConfig configuration; - private DataRefreshPriorityEnum _refreshPriority = DataRefreshPriorityEnum.SCHEDULED; - - protected boolean isOnline() { + private RefreshPriority refreshPriority = RefreshPriority.PRIORITY_NORMAL; - if (zoneMinderSession == null) { - return false; - } - - if (!zoneMinderSession.isConnected()) { - return false; + protected boolean isThingOnline() { + try { + if ((thing.getStatus() == ThingStatus.ONLINE) && getZoneMinderBridgeHandler().isOnline()) { + return true; + } + } catch (Exception ex) { + logger.error("{}: context='isThingOnline' Exception occurred", getLogIdentifier(), ex); } - - return true; + return false; } - public DataRefreshPriorityEnum getRefreshPriority() { - return _refreshPriority; + public RefreshPriority getThingRefreshPriority() { + return refreshPriority; } public ZoneMinderBaseThingHandler(Thing thing) { @@ -100,104 +93,179 @@ public ZoneMinderBaseThingHandler(Thing thing) { */ @Override public void initialize() { - - updateStatus(ThingStatus.ONLINE); - try { - - } catch (Exception ex) { - logger.error("{}: BridgeHandler failed to initialize. Exception='{}'", getLogIdentifier(), ex.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR); - } finally { - } + updateStatus(ThingStatus.OFFLINE); } protected boolean isConnected() { - if (zoneMinderSession == null) { + if ((getThing().getStatus() != ThingStatus.ONLINE) || (zoneMinderConnection == null) + || (getZoneMinderBridgeHandler() == null)) { return false; } - return zoneMinderSession.isConnected(); + return getZoneMinderBridgeHandler().isConnected(); + } + + protected IZoneMinderConnectionHandler aquireSession() { + return aquireSessionInternal(false); } - protected IZoneMinderSession aquireSession() { - lockSession.lock(); - return zoneMinderSession; + protected IZoneMinderConnectionHandler aquireSessionWait() { + return aquireSessionInternal(false); + } + + private IZoneMinderConnectionHandler aquireSessionInternal(boolean timeout) { + boolean result = true; + if (result) { + return zoneMinderConnection; + } + + return null; } protected void releaseSession() { - lockSession.unlock(); + // lockSession.unlock(); + } + + protected boolean forceStartAlarmRefresh() { + lockAlarm.lock(); + try { + if (refreshPriority != RefreshPriority.PRIORITY_ALARM) { + logger.debug("{}: context='startAlarmRefresh' Starting ALARM refresh...", getLogIdentifier()); + refreshPriority = RefreshPriority.PRIORITY_ALARM; + + // If already activated and called again, it is the event from ZoneMidner that was triggered from + // openHAB + alarmTimeoutTimestamp = -1; + } + } finally { + lockAlarm.unlock(); + } + return true; } /** * Method to start a priority data refresh task. */ - protected boolean startPriorityRefresh() { - - logger.info("[MONITOR-{}]: Starting High Priority Refresh", getZoneMinderId()); - _refreshPriority = DataRefreshPriorityEnum.HIGH_PRIORITY; + protected boolean startAlarmRefresh(long timeout) { + lockAlarm.lock(); + try { + if (refreshPriority != RefreshPriority.PRIORITY_ALARM) { + logger.debug("{}: context='startAlarmRefresh' Starting ALARM refresh...", getLogIdentifier()); + refreshPriority = RefreshPriority.PRIORITY_ALARM; + alarmTimeoutTimestamp = System.currentTimeMillis() + timeout * 1000; + } + } finally { + lockAlarm.unlock(); + } return true; } + protected void tryStopAlarmRefresh() { + lockAlarm.lock(); + try { + if ((alarmTimeoutTimestamp == -1) || (refreshPriority != RefreshPriority.PRIORITY_ALARM)) { + return; + } + if (alarmTimeoutTimestamp < System.currentTimeMillis()) { + logger.debug("{}: context='tryStopAlarmRefresh' - Alarm refresh timed out - stopping alarm refresh ...", + getLogIdentifier()); + refreshPriority = RefreshPriority.PRIORITY_NORMAL; + + alarmTimeoutTimestamp = 0; + } + } finally { + lockAlarm.unlock(); + } + } + /** * Method to stop the data Refresh task. */ - protected void stopPriorityRefresh() { - logger.info("{}: Stopping Priority Refresh for Monitor", getLogIdentifier()); - _refreshPriority = DataRefreshPriorityEnum.SCHEDULED; + protected void forceStopAlarmRefresh() { + lockAlarm.lock(); + try { + if (refreshPriority == RefreshPriority.PRIORITY_ALARM) { + logger.debug("{}: context='forceStopAlarmRefresh' Stopping ALARM refresh...", getLogIdentifier()); + refreshPriority = RefreshPriority.PRIORITY_NORMAL; + alarmTimeoutTimestamp = 0; + } + } finally { + lockAlarm.unlock(); + } + } + + protected void onThingStatusChanged(ThingStatus thingStatus) { } @Override public void dispose() { - } /** * Helper method for getting ChannelUID from ChannelId. * */ - public ChannelUID getChannelUIDFromChannelId(String id) { + + public ChannelUID getChannelUIDFromChannelId(@NonNull String id) { Channel ch = thing.getChannel(id); + if (ch == null) { + return null; + } return ch.getUID(); } - protected abstract void onFetchData(); + protected abstract void onFetchData(RefreshPriority refreshPriority); /** * Method to Refresh Thing Handler. */ - public final synchronized void refreshThing(IZoneMinderSession session, DataRefreshPriorityEnum refreshPriority) { - - if ((refreshPriority != getRefreshPriority()) && (!isConnected())) { - return; - } - - if (refreshPriority == DataRefreshPriorityEnum.HIGH_PRIORITY) { - logger.debug("{}: Performing HIGH PRIORITY refresh", getLogIdentifier()); - } else { - logger.debug("{}: Performing refresh", getLogIdentifier()); - } - - if (getZoneMinderBridgeHandler() != null) { - if (isConnected()) { + public final void refreshThing(RefreshPriority refreshPriority) { + boolean isLocked = false; + try { + if (!isConnected()) { + return; + } - logger.debug("{}: refreshThing(): Bridge '{}' Found for Thing '{}'!", getLogIdentifier(), - getThing().getUID(), this.getThing().getUID()); + if (refreshPriority == RefreshPriority.PRIORITY_ALARM) { + if (!lockRefresh.tryLock()) { + logger.warn( + "{}: context='refreshThing' Failed to obtain refresh lock for '{}' - refreshThing skipped!", + getLogIdentifier(), getThing().getUID()); + isLocked = false; + return; + } + } else { + lockRefresh.lock(); + } + isLocked = true; - onFetchData(); + if (getZoneMinderBridgeHandler() != null) { + onFetchData(refreshPriority); + } else { + logger.warn( + "{}: context='refreshThing' - BridgeHandler was not accessible for '{}', skipping refreshThing", + getLogIdentifier(), getThing().getUID()); } - } - Thing thing = getThing(); - List channels = thing.getChannels(); - logger.debug("{}: refreshThing(): Refreshing Thing - {}", getLogIdentifier(), thing.getUID()); + if (!isThingRefreshed()) { + Thing thing = getThing(); + List channels = thing.getChannels(); + logger.debug("{}: context=refreshThing': Refreshing Channels for '{}'", getLogIdentifier(), + thing.getUID()); - for (Channel channel : channels) { - updateChannel(channel.getUID()); + for (Channel channel : channels) { + updateChannel(channel.getUID()); + } + this.channelRefreshDone(); + } + } catch (Exception ex) { + logger.error("{}: context='refreshThing' - Exception when refreshing '{}' ", getLogIdentifier(), + getThing().getUID(), ex); + } finally { + if (isLocked) { + lockRefresh.unlock(); + } } - - this.setThingRefreshed(true); - logger.debug("[{}: refreshThing(): Thing Refreshed - {}", getLogIdentifier(), thing.getUID()); - } /** @@ -205,18 +273,16 @@ public final synchronized void refreshThing(IZoneMinderSession session, DataRefr * * @return zoneMinderBridgeHandler */ - public synchronized ZoneMinderServerBridgeHandler getZoneMinderBridgeHandler() { - + public /* synchronized */ZoneMinderServerBridgeHandler getZoneMinderBridgeHandler() { if (this.zoneMinderBridgeHandler == null) { - Bridge bridge = getBridge(); if (bridge == null) { - logger.debug("{}: getZoneMinderBridgeHandler(): Unable to get bridge!", getLogIdentifier()); + logger.warn("{}: context='getZoneMinderBridgeHandler' - Unable to get bridge!", getLogIdentifier()); return null; } - logger.debug("{}: getZoneMinderBridgeHandler(): Bridge for '{}' - '{}'", getLogIdentifier(), + logger.debug("{}: context='getZoneMinderBridgeHandler' Bridge for '{}' - '{}'", getLogIdentifier(), getThing().getUID(), bridge.getUID()); ThingHandler handler = null; try { @@ -229,7 +295,8 @@ public synchronized ZoneMinderServerBridgeHandler getZoneMinderBridgeHandler() { if (handler instanceof ZoneMinderServerBridgeHandler) { this.zoneMinderBridgeHandler = (ZoneMinderServerBridgeHandler) handler; } else { - logger.debug("{}: getZoneMinderBridgeHandler(): Unable to get bridge handler!", getLogIdentifier()); + logger.debug("{}: context='getZoneMinderBridgeHandler' Unable to get bridge handler!", + getLogIdentifier()); } } @@ -243,11 +310,9 @@ public synchronized ZoneMinderServerBridgeHandler getZoneMinderBridgeHandler() { */ @Override public void updateChannel(ChannelUID channel) { - OnOffType onOffType; - switch (channel.getId()) { case ZoneMinderConstants.CHANNEL_ONLINE: - updateState(channel, getChannelBoolAsOnOffState(isOnline())); + updateState(channel, getChannelBoolAsOnOffState(isThingOnline())); break; default: logger.error( @@ -261,33 +326,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionInfo connection) - throws IllegalArgumentException, GeneralSecurityException, IOException, ZoneMinderUrlNotFoundException { - lockSession.lock(); - try { - zoneMinderSession = ZoneMinderFactory.CreateSession(connection); - - } finally { - lockSession.unlock(); - } + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + super.bridgeStatusChanged(bridgeStatusInfo); } @Override public void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge) { - - if (bridge.getThing().getUID().equals(getThing().getBridgeUID())) { - - this.setThingRefreshed(false); - } - - lockSession.lock(); - try { - zoneMinderSession = null; - - } finally { - lockSession.unlock(); - } - + zoneMinderConnection = null; } /** @@ -316,7 +361,8 @@ public Channel getChannel(ChannelUID channelUID) { * @return thingRefresh */ public boolean isThingRefreshed() { - return thingRefreshed; + return (thingRefresh.get() > 0) ? false : true; + // return (thingRefresh > 0) ? false : true; } /** @@ -324,8 +370,15 @@ public boolean isThingRefreshed() { * * @param {boolean} refreshed Sets status refreshed of thing */ - public void setThingRefreshed(boolean refreshed) { - this.thingRefreshed = refreshed; + public void requestChannelRefresh() { + thingRefresh.incrementAndGet(); + // this.thingRefresh = this.thingRefresh + 1; + } + + public void channelRefreshDone() { + if (thingRefresh.decrementAndGet() < 0) { + thingRefresh.set(0); + } } protected abstract String getZoneMinderThingType(); @@ -367,7 +420,8 @@ protected State getChannelStringAsStringState(String channelValue) { } } catch (Exception ex) { - logger.error("{}", ex.getMessage()); + + logger.error("{}: Exception occurred in 'getChannelStringAsStringState' ", getLogIdentifier(), ex); } return state; @@ -383,25 +437,28 @@ protected State getChannelBoolAsOnOffState(boolean value) { } } catch (Exception ex) { - logger.error("{}: Exception occurred in 'getChannelBoolAsOnOffState()' (Exception='{}')", - getLogIdentifier(), ex.getMessage()); + logger.error("{}: Exception occurred in 'getChannelBoolAsOnOffState()' ", getLogIdentifier(), ex); } return state; } + @Override + public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionHandler connection) { + zoneMinderConnection = connection; + } + @Override public abstract String getLogIdentifier(); protected void updateThingStatus(ThingStatus thingStatus, ThingStatusDetail statusDetail, String statusDescription) { - ThingStatusInfo curStatusInfo = thing.getStatusInfo(); String curDescription = ((curStatusInfo.getDescription() == null) ? "" : curStatusInfo.getDescription()); - // Status changed - if ((curStatusInfo.getStatus() != thingStatus) || (curStatusInfo.getStatusDetail() != statusDetail) - || (curDescription != statusDescription)) { + // Status changed + if (!curStatusInfo.getStatus().equals(thingStatus) || !curStatusInfo.getStatusDetail().equals(statusDetail) + || !curDescription.equals(statusDescription)) { // Update Status correspondingly if ((thingStatus == ThingStatus.OFFLINE) && (statusDetail != ThingStatusDetail.NONE)) { logger.info("{}: Thing status changed from '{}' to '{}' (DetailedStatus='{}', Description='{}')", @@ -412,7 +469,8 @@ protected void updateThingStatus(ThingStatus thingStatus, ThingStatusDetail stat thingStatus); updateStatus(thingStatus); } + + onThingStatusChanged(thingStatus); } } - } diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderServerBridgeHandler.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderServerBridgeHandler.java index 3a68a6bd0c5b9..ea19d7a22e5b9 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderServerBridgeHandler.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderServerBridgeHandler.java @@ -10,6 +10,9 @@ import java.io.IOException; import java.math.BigDecimal; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Hashtable; @@ -19,9 +22,9 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.security.auth.login.FailedLoginException; - import org.apache.commons.lang.StringUtils; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.smarthome.config.core.Configuration; import org.eclipse.smarthome.config.discovery.DiscoveryService; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; @@ -33,7 +36,6 @@ import org.eclipse.smarthome.core.thing.ThingStatusDetail; import org.eclipse.smarthome.core.thing.ThingStatusInfo; import org.eclipse.smarthome.core.thing.ThingTypeUID; -import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; @@ -41,7 +43,8 @@ import org.eclipse.smarthome.core.types.UnDefType; import org.openhab.binding.zoneminder.ZoneMinderConstants; import org.openhab.binding.zoneminder.ZoneMinderProperties; -import org.openhab.binding.zoneminder.internal.DataRefreshPriorityEnum; +import org.openhab.binding.zoneminder.internal.RefreshPriority; +import org.openhab.binding.zoneminder.internal.ZoneMinderConnectionStatus; import org.openhab.binding.zoneminder.internal.config.ZoneMinderBridgeServerConfig; import org.openhab.binding.zoneminder.internal.discovery.ZoneMinderDiscoveryService; import org.osgi.framework.ServiceRegistration; @@ -50,28 +53,35 @@ import com.google.common.collect.Sets; -import name.eskildsen.zoneminder.IZoneMinderConnectionInfo; -import name.eskildsen.zoneminder.IZoneMinderDaemonStatus; -import name.eskildsen.zoneminder.IZoneMinderDiskUsage; -import name.eskildsen.zoneminder.IZoneMinderHostLoad; -import name.eskildsen.zoneminder.IZoneMinderHostVersion; -import name.eskildsen.zoneminder.IZoneMinderMonitorData; +import name.eskildsen.zoneminder.IZoneMinderConnectionHandler; +import name.eskildsen.zoneminder.IZoneMinderEventSession; import name.eskildsen.zoneminder.IZoneMinderServer; -import name.eskildsen.zoneminder.IZoneMinderSession; import name.eskildsen.zoneminder.ZoneMinderFactory; -import name.eskildsen.zoneminder.api.config.ZoneMinderConfig; -import name.eskildsen.zoneminder.api.config.ZoneMinderConfigEnum; +import name.eskildsen.zoneminder.common.ZoneMinderConfigEnum; +import name.eskildsen.zoneminder.data.IMonitorDataGeneral; +import name.eskildsen.zoneminder.data.IZoneMinderDaemonStatus; +import name.eskildsen.zoneminder.data.IZoneMinderDiskUsage; +import name.eskildsen.zoneminder.data.IZoneMinderHostLoad; +import name.eskildsen.zoneminder.data.IZoneMinderHostVersion; +import name.eskildsen.zoneminder.data.ZoneMinderConfig; +import name.eskildsen.zoneminder.exception.ZoneMinderApiNotEnabledException; +import name.eskildsen.zoneminder.exception.ZoneMinderAuthenticationException; +import name.eskildsen.zoneminder.exception.ZoneMinderException; +import name.eskildsen.zoneminder.exception.ZoneMinderGeneralException; +import name.eskildsen.zoneminder.exception.ZoneMinderInvalidData; +import name.eskildsen.zoneminder.exception.ZoneMinderResponseException; import name.eskildsen.zoneminder.exception.ZoneMinderUrlNotFoundException; /** * Handler for a ZoneMinder Server. * - * @author Martin S. Eskildsen + * @author Martin S. Eskildsen - Initial contribution * */ -public class ZoneMinderServerBridgeHandler extends BaseBridgeHandler implements ZoneMinderHandler { +public class ZoneMinderServerBridgeHandler extends BaseBridgeHandler implements IZoneMinderHandler { public static final int TELNET_TIMEOUT = 5000; + static final int HTTP_TIMEOUT = 5000; public static final Set SUPPORTED_THING_TYPES = Sets .newHashSet(ZoneMinderConstants.THING_TYPE_BRIDGE_ZONEMINDER_SERVER); @@ -79,31 +89,39 @@ public class ZoneMinderServerBridgeHandler extends BaseBridgeHandler implements /** * Logger */ - private final Logger logger = LoggerFactory.getLogger(getClass()); + private Logger logger = LoggerFactory.getLogger(getClass()); + + private ZoneMinderConnectionStatus zmConnectStatus = ZoneMinderConnectionStatus.UNINITIALIZED; + private ZoneMinderConnectionStatus lastSucceededStatus = ZoneMinderConnectionStatus.UNINITIALIZED; - private ZoneMinderDiscoveryService discoveryService = null; - private ServiceRegistration discoveryRegistration = null; + private RefreshPriority forcedPriority = RefreshPriority.UNKNOWN; - private ScheduledFuture taskWatchDog = null; - private int refreshFrequency = 0; - private int refreshCycleCount = 0; + private ZoneMinderDiscoveryService discoveryService; + private ServiceRegistration discoveryRegistration; + + private ScheduledFuture taskWatchDog; + private Integer refreshCycleCount = 0; /** Connection status for the bridge. */ private boolean connected = false; - private ThingStatus curBridgeStatus = ThingStatus.UNKNOWN; - - protected boolean _online = false; private Runnable watchDogRunnable = new Runnable() { - private int watchDogCount = -1; + private int watchDogCount = 0; @Override public void run() { - try { - updateAvaliabilityStatus(zoneMinderConnection); + updateAvaliabilityStatus(getZoneMinderConnection()); + + // Only Discover if Bridge is online + if (thing.getStatusInfo().getStatus() != ThingStatus.ONLINE) { + return; + } - if ((discoveryService != null) && (getBridgeConfig().getAutodiscoverThings() == true)) { + // Check if autodiscovery is enabled + boolean bAutoDiscover = getBridgeConfig().getAutodiscoverThings(); + + if ((discoveryService != null) && bAutoDiscover) { watchDogCount++; // Run every two minutes if ((watchDogCount % 8) == 0) { @@ -111,8 +129,11 @@ public void run() { watchDogCount = 0; } } + } catch (Exception exception) { - logger.error("[WATCHDOG]: Server run(): Exception: {}", exception.getMessage()); + StackTraceElement ste = exception.getStackTrace()[0]; + logger.error("[WATCHDOG]: Server run(): StackTrace: File='{}', Line='{}', Method='{}'", + ste.getFileName(), ste.getLineNumber(), ste.getMethodName(), exception); } } }; @@ -123,112 +144,70 @@ public void run() { private String channelCpuLoad = ""; private String channelDiskUsage = ""; - private Boolean isInitialized = false; + private boolean initialized = false; - private IZoneMinderSession zoneMinderSession = null; - private IZoneMinderConnectionInfo zoneMinderConnection = null; + private IZoneMinderEventSession zoneMinderEventSession; + private IZoneMinderConnectionHandler zoneMinderConnection; private ScheduledFuture taskRefreshData = null; - private ScheduledFuture taskPriorityRefreshData = null; - - private Runnable refreshDataRunnable = () -> { - try { - boolean fetchDiskUsage = false; - - if (!isOnline()) { - logger.debug("{}: Bridge '{}' is noit online skipping refresh", getLogIdentifier(), thing.getUID()); - } - - refreshCycleCount++; - int iMaxCycles; - boolean resetCount = false; - boolean doRefresh = false; + private IZoneMinderConnectionHandler getZoneMinderConnection() { + return zoneMinderConnection; + } - // Disk Usage is disabled - if (getBridgeConfig().getRefreshIntervalLowPriorityTask() == 0) { - iMaxCycles = getBridgeConfig().getRefreshInterval(); - resetCount = true; - doRefresh = true; - } else { - iMaxCycles = getBridgeConfig().getRefreshIntervalLowPriorityTask() * 60; - doRefresh = true; - if ((refreshCycleCount * refreshFrequency) >= (getBridgeConfig().getRefreshIntervalLowPriorityTask() - * 60)) { - fetchDiskUsage = true; - resetCount = true; + private Runnable refreshDataRunnable = new Runnable() { + @Override + public void run() { + try { + if (!isConnected()) { + return; + } + refreshCycleCount++; + int intervalBatch = 3600; + int intervalLow = getBridgeConfig().getRefreshIntervalLow(); + int intervalNormal = getBridgeConfig().getRefreshIntervalNormal(); + int intervalHigh = 5; + + RefreshPriority cyclePriority = RefreshPriority.PRIORITY_ALARM; + + // boolean isBatch = ((refreshCycleCount % intervalBatch) == 0); + boolean isLow = ((refreshCycleCount % intervalLow) == 0); + boolean isNormal = ((refreshCycleCount % intervalNormal) == 0); + boolean isHigh = ((refreshCycleCount % intervalHigh) == 0); + + if (isLow) { + cyclePriority = RefreshPriority.PRIORITY_BATCH; + } else if (isLow) { + cyclePriority = RefreshPriority.PRIORITY_LOW; + } else if (isNormal) { + cyclePriority = RefreshPriority.PRIORITY_NORMAL; + } else if (isHigh) { + cyclePriority = RefreshPriority.PRIORITY_HIGH; } - } - logger.debug( - "{}: Running Refresh data task count='{}', freq='{}', max='{}', interval='{}', intervalLow='{}'", - getLogIdentifier(), refreshCycleCount, refreshFrequency, iMaxCycles, - getBridgeConfig().getRefreshInterval(), getBridgeConfig().getRefreshIntervalLowPriorityTask()); + refreshThing(cyclePriority); - if (doRefresh) { - if (resetCount == true) { + if ((refreshCycleCount >= intervalLow) && (refreshCycleCount >= intervalNormal) + && (refreshCycleCount >= intervalHigh) && (refreshCycleCount >= intervalBatch)) { refreshCycleCount = 0; } - logger.debug("{}: 'refreshDataRunnable()': (diskUsage='{}')", getLogIdentifier(), fetchDiskUsage); - - refreshThing(zoneMinderSession, fetchDiskUsage); - } - } catch (Exception exception) { - logger.error("{}: monitorRunnable::run(): Exception: ", getLogIdentifier(), exception); - } - }; - - private Runnable refreshPriorityDataRunnable = () -> { - try { - // Make sure priority updates is done - for (Thing thing : getThing().getThings()) { - try { - if (thing.getThingTypeUID().equals(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR)) { - Thing thingMonitor = thing; - - ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler(); - if (thingHandler != null) { - - if (thingHandler.getRefreshPriority() == DataRefreshPriorityEnum.HIGH_PRIORITY) { - logger.debug("[MONITOR-{}]: RefreshPriority is High Priority", - thingHandler.getZoneMinderId()); - thingHandler.refreshThing(zoneMinderSession, DataRefreshPriorityEnum.HIGH_PRIORITY); - } - } else { - logger.debug( - "[MONITOR]: refreshThing not called for monitor, since thingHandler is 'null'"); - } - } - } catch (NullPointerException ex) { - // This isn't critical (unless it comes over and over). There seems to be a bug so that a - // null - // pointer exception is coming every now and then. - // HAve to find the reason for that. Until thenm, don't Spamm - logger.error("[MONITOR]: Method 'refreshThing()' for Bridge failed for thing='{}' - Exception: ", - thing.getUID(), ex); - } catch (Exception ex) { - logger.error("[MONITOR]: Method 'refreshThing()' for Bridge failed for thing='{}' - Exception: ", - thing.getUID(), ex); - } + } catch (Exception exception) { + logger.error("{}: monitorRunnable::run()", getLogIdentifier(), exception); } - } catch (Exception exception) { - logger.error("[MONITOR]: monitorRunnable::run(): Exception: ", exception); } }; /** * Constructor * - * - * @param bridge - * Bridge object representing a ZoneMinder Server + * @param bridge Bridge object representing a ZoneMinder Server */ public ZoneMinderServerBridgeHandler(Bridge bridge) { super(bridge); - logger.info("{}: Starting ZoneMinder Server Bridge Handler (Bridge='{}')", getLogIdentifier(), - bridge.getBridgeUID()); + logger.info("{}: context='constructor' Starting ZoneMinder Server Bridge Handler (Bridge='{}')", + getLogIdentifier(), bridge.getBridgeUID()); } /** @@ -236,65 +215,54 @@ public ZoneMinderServerBridgeHandler(Bridge bridge) { */ @Override public void initialize() { - logger.debug("[BRIDGE]: About to initialize bridge " + ZoneMinderConstants.BRIDGE_ZONEMINDER_SERVER); try { - updateStatus(ThingStatus.OFFLINE); - logger.info("BRIDGE: ZoneMinder Server Bridge Handler Initialized"); - logger.debug("BRIDGE: HostName: {}", getBridgeConfig().getHostName()); - logger.debug("BRIDGE: Protocol: {}", getBridgeConfig().getProtocol()); - logger.debug("BRIDGE: Port HTTP(S) {}", getBridgeConfig().getHttpPort()); - logger.debug("BRIDGE: Port Telnet {}", getBridgeConfig().getTelnetPort()); - logger.debug("BRIDGE: Server Path {}", getBridgeConfig().getServerBasePath()); - logger.debug("BRIDGE: User: {}", getBridgeConfig().getUserName()); - logger.debug("BRIDGE: Refresh interval: {}", getBridgeConfig().getRefreshInterval()); - logger.debug("BRIDGE: Low prio. refresh: {}", getBridgeConfig().getRefreshIntervalLowPriorityTask()); - logger.debug("BRIDGE: Autodiscovery: {}", getBridgeConfig().getAutodiscoverThings()); - - closeConnection(); - - zoneMinderConnection = ZoneMinderFactory.CreateConnection(getBridgeConfig().getProtocol(), - getBridgeConfig().getHostName(), getBridgeConfig().getHttpPort(), getBridgeConfig().getTelnetPort(), - getBridgeConfig().getServerBasePath(), getBridgeConfig().getUserName(), - getBridgeConfig().getPassword(), 3000); + zoneMinderConnection = null; taskRefreshData = null; - taskPriorityRefreshData = null; - } catch (Exception ex) { - logger.error("[BRIDGE]: 'ZoneMinderServerBridgeHandler' failed to initialize. Exception='{}'", - ex.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR); - } finally { + updateStatus(ThingStatus.OFFLINE); + + ZoneMinderBridgeServerConfig config = getBridgeConfig(); + logger.info("{}: ZoneMinder Server Bridge Handler Initialized", getLogIdentifier()); + logger.debug("{}: HostName: {}", getLogIdentifier(), config.getHost()); + logger.debug("{}: Protocol: {}", getLogIdentifier(), config.getProtocol()); + logger.debug("{}: Port HTTP(S) {}", getLogIdentifier(), config.getHttpPort()); + logger.debug("{}: Port Telnet {}", getLogIdentifier(), config.getTelnetPort()); + logger.debug("{}: Portal Path {}", getLogIdentifier(), config.getServerBasePath()); + logger.debug("{}: API Path {}", getLogIdentifier(), config.getServerApiPath()); + logger.debug("{}: Refresh interval: {}", getLogIdentifier(), config.getRefreshIntervalNormal()); + logger.debug("{}: Low prio. refresh: {}", getLogIdentifier(), config.getRefreshIntervalLow()); + logger.debug("{}: Autodiscovery: {}", getLogIdentifier(), config.getAutodiscoverThings()); + startWatchDogTask(); - isInitialized = true; - } - } + initialized = true; + return; + } catch (Exception ex) { + if (zoneMinderConnection == null) { + logger.error( + "{}: 'ZoneMinderServerBridgeHandler' general configuration error occurred. Failed to initialize.", + getLogIdentifier(), ex); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); + } else { + logger.error("{}: 'ZoneMinderServerBridgeHandler' failed to initialize", getLogIdentifier(), ex); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR); + } - /** - * Method to find the lowest possible refresh rate (based on configuration) - * - * @param refreshRate - * @return - */ - protected int calculateCommonRefreshFrequency(int refreshRate) { - // Check if 30, 15, 10 or 5 seconds is possible - if ((refreshRate % 30) == 0) { - return 30; - } else if ((refreshRate % 15) == 0) { - return 15; - } else if ((refreshRate % 10) == 0) { - return 10; - } else if ((refreshRate % 5) == 0) { - return 5; } - // Hmm, didn't find a obvious shared value. Run every second... - return 1; + initialized = false; + + } + protected IZoneMinderConnectionHandler aquireSession() { + return zoneMinderConnection; + } + + protected void releaseSession() { } protected void startWatchDogTask() { - taskWatchDog = startTask(watchDogRunnable, 0, 15, TimeUnit.SECONDS); + taskWatchDog = startTask(watchDogRunnable, 5, 1, TimeUnit.SECONDS); } protected void stopWatchDogTask() { @@ -302,32 +270,47 @@ protected void stopWatchDogTask() { taskWatchDog = null; } + @Override + public void handleConfigurationUpdate(Map configurationParameters) { + try { + setConnected(false); + zoneMinderConnection = null; + setConnectionStatus(ZoneMinderConnectionStatus.UNINITIALIZED); + } catch (IllegalArgumentException | GeneralSecurityException | IOException | ZoneMinderUrlNotFoundException e) { + logger.error("{}: context='handleConfigurationUpdate'", getLogIdentifier(), e.getCause()); + } + super.handleConfigurationUpdate(configurationParameters); + } + + @Override + protected void updateConfiguration(Configuration configuration) { + super.updateConfiguration(configuration); + // Inform thing handlers of connection + } + /** */ @Override public void dispose() { - try { - logger.debug("{}: Stop polling of ZoneMinder Server API", getLogIdentifier()); + logger.debug("{}: context='dispose' Stop polling of ZoneMinder Server API", getLogIdentifier()); - logger.info("{}: Stopping Discovery service", getLogIdentifier()); - // Remove the discovery service - if (discoveryService != null) { - discoveryService.deactivate(); - discoveryService = null; - } + logger.info("{}: context='dispose' Stopping Discovery service", getLogIdentifier()); + // Remove the discovery service + if (discoveryService != null) { + discoveryService.deactivate(); + discoveryService = null; + } - if (discoveryRegistration != null) { - discoveryRegistration.unregister(); - discoveryRegistration = null; - } + if (discoveryRegistration != null) { + discoveryRegistration.unregister(); + discoveryRegistration = null; + } - logger.info("{}: Stopping WatchDog task", getLogIdentifier()); - stopWatchDogTask(); + logger.info("{}: context='dispose' Stopping WatchDog task", getLogIdentifier()); + stopWatchDogTask(); - logger.info("{}: Stopping refresh data task", getLogIdentifier()); - stopTask(taskRefreshData); - } catch (Exception ex) { - } + logger.info("{}: context='dispose' Stopping refresh data task", getLogIdentifier()); + stopTask(taskRefreshData); } protected String getThingId() { @@ -336,33 +319,21 @@ protected String getThingId() { @Override public String getZoneMinderId() { - return getThing().getUID().getAsString(); } - @Override - public void channelLinked(ChannelUID channelUID) { - // can be overridden by subclasses - ThingUID s1 = getThing().getUID(); - ThingTypeUID s2 = getThing().getThingTypeUID(); - logger.debug("{}: Channel '{}' was linked to '{}'", getLogIdentifier(), channelUID.getAsString(), - this.thing.getThingTypeUID()); - } - - @Override - public void channelUnlinked(ChannelUID channelUID) { - // can be overridden by subclasses - logger.debug("{}: Channel '{}' was unlinked from '{}'", getLogIdentifier(), channelUID.getAsString(), - this.thing.getThingTypeUID()); - } - - protected ArrayList getMonitors(IZoneMinderSession session) { - + protected ArrayList getMonitors(IZoneMinderConnectionHandler session) + throws ZoneMinderAuthenticationException { if (isConnected()) { - return ZoneMinderFactory.getServerProxy(session).getMonitors(); - } + try { + return ZoneMinderFactory.getServerProxy(session).getMonitors(); + } catch (ZoneMinderGeneralException | ZoneMinderResponseException | ZoneMinderInvalidData ex) { + logger.error("{}: context='getMonitors' Exception {}", getLogIdentifier(), ex.getMessage(), + ex.getCause()); - return new ArrayList(); + } + } + return new ArrayList<>(); } @@ -375,12 +346,12 @@ protected ZoneMinderBridgeServerConfig getBridgeConfig() { */ public ZoneMinderBaseThingHandler getZoneMinderThingHandlerFromZoneMinderId(ThingTypeUID thingTypeUID, String zoneMinderId) { - // Inform thing handlers of connection List things = getThing().getThings(); for (Thing thing : things) { ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler(); + if ((thingHandler.getZoneMinderId().equals(zoneMinderId)) && (thing.getThingTypeUID().equals(thingTypeUID))) { return thingHandler; @@ -394,84 +365,121 @@ public void handleCommand(ChannelUID channelUID, Command command) { logger.debug("{}: Update '{}' with '{}'", getLogIdentifier(), channelUID.getAsString(), command.toString()); } - protected synchronized void refreshThing(IZoneMinderSession session, boolean fetchDiskUsage) { + protected /* synchronized */ void refreshThing(RefreshPriority refresh) { + IZoneMinderServer zoneMinderServerProxy = null; + RefreshPriority curPriority = RefreshPriority.DISABLED; - logger.debug("{}: 'refreshThing()': Thing='{}'!", getLogIdentifier(), this.getThing().getUID()); + // logger.debug("{}: 'refreshThing()': Thing='{}'!", getLogIdentifier(), this.getThing().getUID()); List channels = getThing().getChannels(); List things = getThing().getThings(); - IZoneMinderServer zoneMinderServerProxy = ZoneMinderFactory.getServerProxy(session); - if (zoneMinderServerProxy == null) { - logger.warn("{}: Could not obtain ZonerMinderServerProxy ", getLogIdentifier()); + try { + if (forcedPriority == RefreshPriority.UNKNOWN) { + return; + } else if (forcedPriority == RefreshPriority.DISABLED) { + curPriority = refresh; + } else { + curPriority = forcedPriority; + forcedPriority = RefreshPriority.DISABLED; + } - // Make sure old data is cleared - channelCpuLoad = ""; - channelDiskUsage = ""; + zoneMinderServerProxy = ZoneMinderFactory.getServerProxy(aquireSession()); + if (zoneMinderServerProxy == null) { + logger.warn("{}: Could not obtain ZonerMinderServerProxy ", getLogIdentifier()); - } else if (isConnected()) { - /* - * Fetch data for Bridge - */ - IZoneMinderHostLoad hostLoad = null; - try { - hostLoad = zoneMinderServerProxy.getHostCpuLoad(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - zoneMinderServerProxy.getHttpUrl(), zoneMinderServerProxy.getHttpResponseCode(), - zoneMinderServerProxy.getHttpResponseMessage()); + // Make sure old data is cleared + channelCpuLoad = ""; + channelDiskUsage = ""; - } catch (FailedLoginException | ZoneMinderUrlNotFoundException | IOException ex) { - logger.error("{}: Exception thrown in call to ZoneMinderHostLoad: ", getLogIdentifier(), ex); - } + } else if (isConnected()) { + /* + * Fetch data for Bridge + */ + if (curPriority.isPriorityActive(RefreshPriority.PRIORITY_NORMAL)) { + IZoneMinderHostLoad hostLoad = null; + try { + hostLoad = zoneMinderServerProxy.getHostCpuLoad(); + logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), + hostLoad.getHttpRequestUrl(), hostLoad.getHttpStatus(), + hostLoad.getHttpResponseMessage()); + + } catch (ZoneMinderUrlNotFoundException | IOException ex) { + logger.error("{}: Exception thrown in call to ZoneMinderHostLoad ('{}')", getLogIdentifier(), + ex.getMessage()); + } catch (ZoneMinderException ex) { + logger.error( + "{}: context='refreshThing' error in call to 'getHostCpuLoad' ExceptionClass='{}' - Message='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex.getMessage(), ex.getCause()); + } - if (hostLoad == null) { - logger.warn("{}: ZoneMinderHostLoad dataset could not be obtained (received 'null')", - getLogIdentifier()); - } else if (hostLoad.getHttpResponseCode() != 200) { - logger.warn( - "BRIDGE [{}]: ZoneMinderHostLoad dataset could not be obtained (HTTP Response: Code='{}', Message='{}')", - getThingId(), hostLoad.getHttpResponseCode(), hostLoad.getHttpResponseMessage()); + if (hostLoad == null) { + logger.warn("{}: ZoneMinderHostLoad dataset could not be obtained (received 'null')", + getLogIdentifier()); + } else if (hostLoad.getHttpStatus() != HttpStatus.OK_200) { + logger.warn( + "{}: ZoneMinderHostLoad dataset could not be obtained (HTTP Response: Code='{}', Message='{}')", + getLogIdentifier(), hostLoad.getHttpStatus(), hostLoad.getHttpResponseMessage()); - } else { - channelCpuLoad = hostLoad.getCpuLoad().toString(); - } + } else { + channelCpuLoad = hostLoad.getCpuLoad().toString(); + } - if (fetchDiskUsage) { - IZoneMinderDiskUsage diskUsage = null; - try { - diskUsage = zoneMinderServerProxy.getHostDiskUsage(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - zoneMinderServerProxy.getHttpUrl(), zoneMinderServerProxy.getHttpResponseCode(), - zoneMinderServerProxy.getHttpResponseMessage()); - } catch (Exception ex) { - logger.error("{}: Exception thrown in call to ZoneMinderDiskUsage: ", getLogIdentifier(), ex); - } + if (curPriority.isPriorityActive(getBridgeConfig().getDiskUsageRefresh())) { + IZoneMinderDiskUsage diskUsage = null; + try { + diskUsage = zoneMinderServerProxy.getHostDiskUsage(); + logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), + diskUsage.getHttpRequestUrl(), diskUsage.getHttpStatus(), + diskUsage.getHttpResponseMessage()); + } catch (Exception ex) { + logger.error( + "{}: context='refreshThing' Exception thrown in call to ZoneMinderDiskUsage ('{}')", + getLogIdentifier(), ex.getMessage()); + } catch (ZoneMinderException ex) { + logger.error( + "{}: context='refreshThing' error in call to 'getHostDiskUsage' ExceptionClass='{}' - Message='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex.getMessage(), + ex.getCause()); + } - if (diskUsage == null) { - logger.warn("{}: ZoneMinderDiskUsage dataset could not be obtained (received 'null')", - getLogIdentifier()); - } else if (hostLoad.getHttpResponseCode() != 200) { - logger.warn( - "{}: ZoneMinderDiskUsage dataset could not be obtained (HTTP Response: Code='{}', Message='{}')", - getLogIdentifier(), hostLoad.getHttpResponseCode(), hostLoad.getHttpResponseMessage()); + if (diskUsage == null) { + logger.warn( + "{}: context='refreshThing' ZoneMinderDiskUsage dataset could not be obtained (received 'null')", + getLogIdentifier()); + } else if (diskUsage.getHttpStatus() != HttpStatus.OK_200) { + logger.warn( + "{}: context='refreshThing' ZoneMinderDiskUsage dataset could not be obtained (HTTP Response: Code='{}', Message='{}')", + getLogIdentifier(), diskUsage.getHttpStatus(), diskUsage.getHttpResponseMessage()); - } else { - channelDiskUsage = diskUsage.getDiskUsage(); + } else { + channelDiskUsage = diskUsage.getDiskUsage(); + } + } } + } else { + // Make sure old data is cleared + channelCpuLoad = ""; + channelDiskUsage = ""; + } + } catch (Exception ex) { + logger.error("{}: context='refreshThing' tag='exception' Exception thrown when refreshing bridge='{}'", + getLogIdentifier(), getThing().getBridgeUID(), ex); + } finally { + if (zoneMinderServerProxy != null) { + releaseSession(); } - } else { - _online = false; - // Make sure old data is cleared - channelCpuLoad = ""; - channelDiskUsage = ""; } /* * Update all channels on Bridge */ for (Channel channel : channels) { - updateChannel(channel.getUID()); + + if (isLinked(channel.getUID().getId())) { + updateChannel(channel.getUID()); + } } /* @@ -479,26 +487,18 @@ protected synchronized void refreshThing(IZoneMinderSession session, boolean fet */ for (Thing thing : things) { try { - if (thing.getThingTypeUID().equals(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR)) { - Thing thingMonitor = thing; ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler(); - thingHandler.refreshThing(session, DataRefreshPriorityEnum.SCHEDULED); + if (thingHandler != null) { + thingHandler.refreshThing(curPriority); + } } - - } catch (NullPointerException ex) { - // This isn't critical (unless it comes over and over). There seems to be a bug so that a null - // pointer exception is coming every now and then. - // HAve to find the reason for that. Until thenm, don't Spamm - logger.debug("{}: Method 'refreshThing()' for Bridge {} failed for thing='{}' - Exception='{}'", - getLogIdentifier(), this.getZoneMinderId(), thing.getUID(), ex.getMessage()); - - // Other exceptions has to be shown as errors } catch (Exception ex) { - logger.error("{}: Method 'refreshThing()' for Bridge {} failed for thing='{}' - Exception='{}'", - getLogIdentifier(), this.getZoneMinderId(), thing.getUID(), ex.getMessage()); + logger.error("{}: context='refreshThing' tag='exception' Exception thrown when refreshing thing='{}'", + getLogIdentifier(), thing.getUID(), ex.getCause()); } + } } @@ -511,63 +511,50 @@ public synchronized Boolean isConnected() { } public boolean isOnline() { - return _online; - } - - private synchronized boolean getConnected() { - return this.connected; + ThingStatusInfo statusInfo = getThing().getStatusInfo(); + return (statusInfo.getStatus() == ThingStatus.ONLINE) ? true : false; } /** * Set connection status. * * @param connected + * @throws ZoneMinderUrlNotFoundException + * @throws IOException + * @throws GeneralSecurityException + * @throws IllegalArgumentException */ - private synchronized void setConnected(boolean connected) { - + private void setConnected(boolean connected) + throws IllegalArgumentException, GeneralSecurityException, IOException, ZoneMinderUrlNotFoundException { if (this.connected != connected) { - if (connected) { - try { - zoneMinderSession = ZoneMinderFactory.CreateSession(zoneMinderConnection); - } catch (FailedLoginException | IllegalArgumentException | IOException - | ZoneMinderUrlNotFoundException e) { - logger.error("BRIDGE [{}]: Call to setConencted failed with exception '{}'", getThingId(), - e.getMessage()); - } - } else { - zoneMinderSession = null; - } - this.connected = connected; - - } + try { + if (connected) { + try { + if (zoneMinderEventSession == null) { + zoneMinderEventSession = ZoneMinderFactory.CreateEventSession(zoneMinderConnection); + } - } + } catch (Exception ex) { + zoneMinderEventSession = null; + return; + } - /** - * Set channel 'bridge_connection'. - * - * @param connected - */ - private void setBridgeConnectionStatus(boolean connected) { - logger.debug(" {}: setBridgeConnection(): Set Bridge to {}", getLogIdentifier(), - connected ? ThingStatus.ONLINE : ThingStatus.OFFLINE); - - Bridge bridge = getBridge(); - if (bridge != null) { - ThingStatus status = bridge.getStatus(); - logger.debug("{}: Bridge ThingStatus is: {}", getLogIdentifier(), status); + } else { + if (zoneMinderEventSession != null) { + zoneMinderEventSession.unsubscribeAllMonitorEvents(); + } + zoneMinderConnection = null; + zoneMinderEventSession = null; + } + } finally { + this.connected = connected; + if (connected) { + onConnected(); + } else { + onDisconnected(); + } + } } - - setConnected(connected); - } - - /** - * Set channel 'bridge_connection'. - * - * @param connected - */ - private boolean getBridgeConnectionStatus() { - return getConnected(); } /** @@ -578,10 +565,10 @@ private boolean getBridgeConnectionStatus() { * @throws GeneralSecurityException * @throws IllegalArgumentException */ - public void onConnected() { - logger.debug("BRIDGE [{}]: onConnected(): Bridge Connected!", getThingId()); - setConnected(true); - onBridgeConnected(this, zoneMinderConnection); + public void onConnected() + throws IllegalArgumentException, GeneralSecurityException, IOException, ZoneMinderUrlNotFoundException { + logger.debug("{}: [{}]: onConnected(): Bridge Connected!", getLogIdentifier(), getThingId()); + onBridgeConnected(this, getZoneMinderConnection()); // Inform thing handlers of connection List things = getThing().getThings(); @@ -591,23 +578,24 @@ public void onConnected() { if (thingHandler != null) { try { - thingHandler.onBridgeConnected(this, zoneMinderConnection); - } catch (IllegalArgumentException | GeneralSecurityException | IOException - | ZoneMinderUrlNotFoundException e) { - logger.error("{}: onConnected() failed - Exceprion: {}", getLogIdentifier(), e.getMessage()); + thingHandler.onBridgeConnected(this, getZoneMinderConnection()); + } catch (IllegalArgumentException e) { + logger.error("{}: context='onConnected' failed - Exceprion: {}", getLogIdentifier(), + e.getMessage()); } - logger.debug("{}: onConnected(): Bridge - {}, Thing - {}, Thing Handler - {}", getLogIdentifier(), - thing.getBridgeUID(), thing.getUID(), thingHandler); + logger.debug("{}: context='onConnected': Bridge - {}, Thing - {}, Thing Handler - {}", + getLogIdentifier(), thing.getBridgeUID(), thing.getUID(), thingHandler); } } + } /** * Runs when disconnected. */ private void onDisconnected() { - logger.debug("{}: onDisconnected(): Bridge Disconnected!", getLogIdentifier()); - setConnected(false); + logger.debug("{}: context='onDisconnected': Bridge Disconnected!", getLogIdentifier()); + onBridgeDisconnected(this); // Inform thing handlers of disconnection @@ -618,276 +606,692 @@ private void onDisconnected() { if (thingHandler != null) { thingHandler.onBridgeDisconnected(this); - logger.debug("{}: onDisconnected(): Bridge - {}, Thing - {}, Thing Handler - {}", getLogIdentifier(), - thing.getBridgeUID(), thing.getUID(), thingHandler); + logger.debug("{}: context='onDisconnected': Bridge - {}, Thing - {}, Thing Handler - {}", + getLogIdentifier(), thing.getBridgeUID(), thing.getUID(), thingHandler); } } } - @Override - public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { + private int initRetriesCount = 0; + private static int initMaxRevoverableRetries = 5; + private static int initMaxUnrecoverableRetries = 5; + private ZoneMinderConnectionStatus verifyBindingConfiguration(ThingStatus currentStatus) { + String context = "verifyBindingConfiguration"; ThingStatus newStatus = ThingStatus.OFFLINE; ThingStatusDetail statusDetail = ThingStatusDetail.NONE; String statusDescription = ""; - boolean _isOnline = false; + ZoneMinderConnectionStatus status = ZoneMinderConnectionStatus.BINDING_CONFIG_INVALID; + ZoneMinderBridgeServerConfig config = null; + try { + // Is it a retry loop? (Is this step already verified?) + // Or is there an unrecoverable error? + if ((getLastSucceededStatus().greatherThanEqual(ZoneMinderConnectionStatus.BINDING_CONFIG_LOAD_PASSED)) + || (getLastSucceededStatus().hasUnrecoverableError())) { + return getLastSucceededStatus(); + } - ThingStatus prevStatus = getThing().getStatus(); + // get Bridge Config + config = getBridgeConfig(); + + // Check if server Bridge configuration is valid + if (config == null) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Configuration not found"; + updateStatus(newStatus, statusDetail, statusDescription); + + logger.error("{}: context='{}' state='{}' check='FAILED' - {}", getLogIdentifier(), context, + newStatus.toString(), statusDescription); + + return status; + + } else if (config.getHost() == null) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Host not configured"; + + updateStatus(newStatus, statusDetail, statusDescription); + + logger.error("{}: context='{}' state='{}' check='FAILED' - {}", getLogIdentifier(), context, + newStatus.toString(), statusDescription); + + return status; + } else if (config.getProtocol() == null) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Unknown protocol in configuration"; + + updateStatus(newStatus, statusDetail, statusDescription); + + logger.error("{}: context='{}' state='{}' check='FAILED' - {}", getLogIdentifier(), context, + newStatus.toString(), statusDescription); + return status; + } + + else if (config.getHttpPort() == null) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "HTTP port invalid in configuration"; + + updateStatus(newStatus, statusDetail, statusDescription); + + logger.error("{}: context='{}' state='{}' check='FAILED' - {}", getLogIdentifier(), context, + newStatus.toString(), statusDescription); + return status; + + } + + else if (config.getTelnetPort() == null) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Telnet port invalid in configuration"; + updateStatus(newStatus, statusDetail, statusDescription); + logger.error("{}: context='{}' state='{}' check='FAILED' - {}", getLogIdentifier(), context, + newStatus.toString(), statusDescription); + return status; + + } + + // Configuration verified + status = ZoneMinderConnectionStatus.BINDING_CONFIG_LOAD_PASSED; + + logger.debug("{}: context='{}' state='{}' check='PASSED'", getLogIdentifier(), context, + newStatus.toString()); + + } catch (Exception ex) { + logger.error("{}: context='{}' state='{}' check='FAILED' tag='exception'", getLogIdentifier(), context, + newStatus.toString(), ex); + + } + return status; + } + + private ZoneMinderConnectionStatus validateConfig(ZoneMinderBridgeServerConfig config) { + String context = "validateConfig"; + ThingStatus newStatus = ThingStatus.OFFLINE; + ThingStatusDetail statusDetail = ThingStatusDetail.NONE; + String statusDescription = ""; + + ZoneMinderConnectionStatus status = ZoneMinderConnectionStatus.GENERAL_ERROR; + + // Is it a retry loop? (Is this step already verified?) + // Or is there an unrecoverable error? + if ((getLastSucceededStatus().greatherThanEqual(ZoneMinderConnectionStatus.BINDING_CONFIG_VALIDATE_PASSED)) + || (getLastSucceededStatus().hasUnrecoverableError())) { + return getLastSucceededStatus(); + } + + // Check if something is responding give host and port HTTP try { - // Just perform a health check to see if we are still connected - if (prevStatus == ThingStatus.ONLINE) { - if (zoneMinderSession == null) { - newStatus = ThingStatus.ONLINE; - statusDetail = ThingStatusDetail.NONE; - statusDescription = ""; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } else if (!zoneMinderSession.isConnected()) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; - statusDescription = "Session lost connection to ZoneMinder Server"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); + // Check Socket + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(config.getHost(), config.getHttpPort()), 5000); + if (socket.isConnected()) { + socket.close(); + } - return; - } + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.NONE; + statusDescription = "Connecting to ZoneMinder Server"; + updateStatus(newStatus, statusDetail, statusDescription); - IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(zoneMinderSession); - IZoneMinderDaemonStatus daemonStatus = serverProxy.getHostDaemonCheckState(); + status = ZoneMinderConnectionStatus.BINDING_CONFIG_VALIDATE_PASSED; + logger.debug("{}: context='{}' previousState='OFFLINE' Socket connection to ZM Website (PASSED)", + getLogIdentifier(), context); - // If service isn't running OR we revceived a http responsecode other than 200, assume we are offline - if ((!daemonStatus.getStatus()) || (daemonStatus.getHttpResponseCode() != 200)) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; - statusDescription = "ZoneMinder Server Daemon not running"; + } catch (UnknownHostException e) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Unknown host - Check configuration"; + status = ZoneMinderConnectionStatus.BINDING_CONNECTION_INVALID; + logger.warn( + "{}: context='{}' tag='exception' previousState='OFFLINE' UnknowHostException when connecting to ZoneMinder Server.", + getLogIdentifier(), context, e); + } catch (IOException e) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Could not contact host - Check configuration"; + updateStatus(newStatus, statusDetail, statusDescription); + + status = ZoneMinderConnectionStatus.BINDING_CONNECTION_TIMEOUT; + logger.warn( + "{}: context='validateConfig' tag='exception' previousState='OFFLINE' Socket connection Timeout.", + getLogIdentifier(), e); + } + return status; + } - logger.debug("{}: {} (state='{}' and ResponseCode='{}')", getLogIdentifier(), statusDescription, - daemonStatus.getStatus(), daemonStatus.getHttpResponseCode()); - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; + private ZoneMinderConnectionStatus getLastSucceededStatus() { + return lastSucceededStatus; + } + + private ZoneMinderConnectionStatus getConnectionStatus() { + return zmConnectStatus; + } + + private void setConnectionStatus(ZoneMinderConnectionStatus newStatus) { + zmConnectStatus = newStatus; + if (!newStatus.isErrorState()) { + lastSucceededStatus = newStatus; + } + } + + private ZoneMinderConnectionStatus validateConnection(ZoneMinderBridgeServerConfig config) { + String context = "validateConnection"; + ThingStatus newStatus = ThingStatus.OFFLINE; + ThingStatusDetail statusDetail = ThingStatusDetail.NONE; + String statusDescription = ""; + + ZoneMinderConnectionStatus status = ZoneMinderConnectionStatus.BINDING_CONNECTION_INVALID; + + // Is it a retry loop? (Is this step already verified?) + // Or is there an unrecoverable error? + if ((getLastSucceededStatus().greatherThanEqual(ZoneMinderConnectionStatus.ZONEMINDER_CONNECTION_CREATED)) + || (getLastSucceededStatus().hasUnrecoverableError())) { + return getLastSucceededStatus(); + } + + try { + aquireSession(); + + if (getZoneMinderConnection() == null) { + zoneMinderConnection = ZoneMinderFactory.CreateConnection(config.getProtocol(), config.getHost(), + config.getHttpPort(), config.getTelnetPort(), config.getUserName(), config.getPassword(), + config.getStreamingUser(), config.getStreamingPassword(), config.getServerBasePath(), + config.getServerApiPath(), HTTP_TIMEOUT); + + logger.debug( + "{}: context='{}' - ZoneMinderFactory.CreateConnection() called (Protocol='{}', Host='{}', HttpPort='{}', SocketPort='{}', Path='{}', API='{}')", + getLogIdentifier(), context, config.getProtocol(), config.getHost(), config.getHttpPort(), + config.getTelnetPort(), config.getServerBasePath(), config.getServerApiPath()); + + } + + } catch (ZoneMinderAuthenticationException authenticationException) { + String detailedMessage = ""; + setConnectionStatus(ZoneMinderConnectionStatus.BINDING_CONFIG_INVALID); + + if (authenticationException.getStackTrace() != null) { + if (authenticationException.getStackTrace().length > 0) { + StackTraceElement ste = authenticationException.getStackTrace()[0]; + detailedMessage = String.format(" StackTrace='%s'", ste.toString()); } - // TODO:: Check other things without being harsh???? + } + logger.error( + "{}: context='{}' check='FAILED' - Failed to login to ZoneMinder Server. Check provided usercredentials (Exception='{}', {})", + getLogIdentifier(), context, authenticationException.getMessage(), detailedMessage, + authenticationException); + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Login to ZoneMinder Server failed. Check provided usercredentials"; + updateStatus(newStatus, statusDetail, statusDescription); + status = ZoneMinderConnectionStatus.SERVER_CREDENTIALS_INVALID; + return status; + + } catch (ZoneMinderApiNotEnabledException e) { + setConnectionStatus(ZoneMinderConnectionStatus.SERVER_API_DISABLED); + logger.error( + "{}: context='{}' check='FAILED' - ZoneMinder Server API is not enabled. Enable option in ZoneMinder Server Settings and restart openHAB Binding.", + getLogIdentifier(), context, e); + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server 'OPT_USE_API' not enabled"; + updateStatus(newStatus, statusDetail, statusDescription); + status = ZoneMinderConnectionStatus.SERVER_API_DISABLED; + return status; + + } catch (ZoneMinderException | Exception e) { + logger.error( + "{}: context='{}' check='FAILED' - General error when creating ConnectionInfo. Retrying next cycle...", + getLogIdentifier(), context, e); + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server Connection error"; + updateStatus(newStatus, statusDetail, statusDescription); + status = ZoneMinderConnectionStatus.GENERAL_ERROR; + return status; - newStatus = ThingStatus.ONLINE; + } finally { + releaseSession(); + } + + // Check that we have a connection + if (getZoneMinderConnection() != null) { + logger.debug("{}: context='{}' previousState='OFFLINE' ZoneMinder Connection check (PASSED)", + getLogIdentifier(), context); + status = ZoneMinderConnectionStatus.ZONEMINDER_CONNECTION_CREATED; + } else { + zoneMinderConnection = null; + status = ZoneMinderConnectionStatus.BINDING_CONNECTION_INVALID; + logger.warn("{}: context='{}' check='FAILED' - Failed to obtain ZoneMinder Connection. Retrying next cycle", + getLogIdentifier(), context); + } + + return status; + } + + public ZoneMinderConnectionStatus validateZoneMinderServerConfig() { + IZoneMinderConnectionHandler curSession = null; + String context = "validateZoneMinderServerConfig"; + ThingStatus newStatus = ThingStatus.OFFLINE; + ThingStatusDetail statusDetail = ThingStatusDetail.NONE; + String statusDescription = ""; + ZoneMinderConnectionStatus status = ZoneMinderConnectionStatus.GENERAL_ERROR; + + IZoneMinderServer serverProxy = null; + try { + curSession = aquireSession(); + if (curSession == null) { + logger.error( + "{}: context='{}' check='FAILED' - Could not verify ZoneMinder Server Config. Session failed to connect.", + getLogIdentifier(), context); + + status = ZoneMinderConnectionStatus.GENERAL_ERROR; + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Session not available"; + updateStatus(newStatus, statusDetail, statusDescription); + return status; + } + + serverProxy = ZoneMinderFactory.getServerProxy(curSession); + + IZoneMinderDaemonStatus daemonStatus = serverProxy.getHostDaemonCheckState(); + + // Check if server API can be accessed + if (!daemonStatus.getStatus()) { + logger.error("{}: context='{}' check='FAILED' - Bridge OFFLINE because server Daemon is not running", + getLogIdentifier(), context); + status = ZoneMinderConnectionStatus.SERVER_DAEMON_NOT_RUNNING; + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server Daemon not running"; + updateStatus(newStatus, statusDetail, statusDescription); + + } + + // Verify that 'OPT_TRIGGER' is set to true in ZoneMinder + else if (!serverProxy.isTriggerOptionEnabled()) { + logger.error( + "{}: context='{}' check='FAILED' - Bridge OFFLINE because ZoneMinder Server option 'OPT_TRIGGERS' not enabled", + getLogIdentifier(), context); + status = ZoneMinderConnectionStatus.SERVER_OPT_TRIGGERS_DISABLED; + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server option 'OPT_TRIGGERS' not enabled"; + updateStatus(newStatus, statusDetail, statusDescription); + + } else { + // Seems like everything is as we want it :-) + logger.debug("{}: context='{}' check='PASSED' - ZoneMinder ", getLogIdentifier(), context); + status = ZoneMinderConnectionStatus.ZONEMINDER_SERVER_CONFIG_PASSED; + + } + + } catch (ZoneMinderException | Exception ex) { + if (ex.getMessage() != null) { + logger.error( + "{}: context='{}' check='FAILED' - ZoneMinder Server configuration failed to verify. (Exception='{}')", + getLogIdentifier(), context, ex.getMessage(), ex.getCause()); + } else { + logger.error("{}: context='{}' check='FAILED' - ZoneMinder Server configuration failed to verify.", + getLogIdentifier(), context, ex.getCause()); + } + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server configuration not verified "; + updateStatus(newStatus, statusDetail, statusDescription); + + } finally { + releaseSession(); + } + return status; + } + + private void initializeAvaliabilityStatus(IZoneMinderConnectionHandler conn) { + String context = "initializeAvaliabilityStatus"; + ZoneMinderBridgeServerConfig config = null; + + ThingStatus newStatus = ThingStatus.OFFLINE; + ThingStatusDetail statusDetail = ThingStatusDetail.NONE; + String statusDescription = ""; + ThingStatus currentStatus = getThing().getStatus(); + + // Only continue if handler is initialized and status is OFFLINE + if ((currentStatus != ThingStatus.OFFLINE) || (!initialized)) { + return; + } + + /******************************** + * + * Retry handling + * + *******************************/ + // An unrecoverable error in the Binding Configuration was found. OR a recoverable has failed and is no + // unrecoverable (try again later) + if (getConnectionStatus().hasUnrecoverableError() || (initRetriesCount > initMaxRevoverableRetries)) { + initRetriesCount++; + + // Reset unrecoverable error and try from beginning + if (initRetriesCount > initMaxRevoverableRetries + initMaxUnrecoverableRetries) { + initRetriesCount = 0; + zoneMinderConnection = null; + zoneMinderEventSession = null; + setConnectionStatus(ZoneMinderConnectionStatus.UNINITIALIZED); + + // Clear old error information + newStatus = ThingStatus.OFFLINE; statusDetail = ThingStatusDetail.NONE; - statusDescription = ""; + statusDescription = "Retrying to connect"; + updateStatus(newStatus, statusDetail, statusDescription); + + logger.debug("{}: context='{}' state='{}' - Retrying initialization after unrecoverable error", + getLogIdentifier(), context, newStatus.toString()); + } - // If we are OFFLINE, check everything - else if (prevStatus == ThingStatus.OFFLINE) { + return; - // Just wait until we are finished initializing - if (isInitialized == false) { - _online = _isOnline; - return; - } + } else if (getConnectionStatus().hasRecoverableError()) { + initRetriesCount++; + logger.debug("{}: context='{}' state='{}' - Retrying initialization (Last Error='{}', Retries='{}')", + getLogIdentifier(), context, newStatus.toString(), getConnectionStatus().toString(), + initRetriesCount); - ZoneMinderBridgeServerConfig config = getBridgeConfig(); + } - // Check if server Bridge configuration is valid - if (config == null) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "Configuration not found"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; + /*********************************** + * + * Verify Binding Configuration + * + ***********************************/ + setConnectionStatus(verifyBindingConfiguration(currentStatus)); + if (!getConnectionStatus().hasPassed(ZoneMinderConnectionStatus.BINDING_CONFIG_LOAD_PASSED)) { + return; + } - } else if (config.getHostName() == null) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "Host not found in configuration"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } else if (config.getProtocol() == null) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "Unknown protocol in configuration"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } + // Great the Bridge Config is valid get started + config = getBridgeConfig(); - else if (config.getHttpPort() == null) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "Invalid HTTP port"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } + /*********************************** + * + * Validate Binding Configuration + * + ***********************************/ + setConnectionStatus(validateConfig(config)); - else if (config.getTelnetPort() == null) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "Invalid telnet port"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } else if (!ZoneMinderFactory.isZoneMinderUrl(connection)) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "URL not a ZoneMinder Server"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } + // Check that Status corresponds actual state + if (!getConnectionStatus().hasPassed(ZoneMinderConnectionStatus.BINDING_CONFIG_VALIDATE_PASSED)) { + return; + } - if (!isZoneMinderLoginValid(connection)) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "Cannot access ZoneMinder Server. Check provided usercredentials"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } + /*********************************** + * + * VALIDATE ZONEMINDER CONNECTION (API + AUTHENTICATION) + * + ***********************************/ + setConnectionStatus(validateConnection(config)); - /* - * Now we will try to establish a session - */ + // A previous step failed. + if (!getConnectionStatus().hasPassed(ZoneMinderConnectionStatus.ZONEMINDER_CONNECTION_CREATED)) { + return; + } + + /*********************************** + * + * VALIDATE ZONEMINDER HTTP Session + * + ***********************************/ + zmConnectStatus = ZoneMinderConnectionStatus.ZONEMINDER_SESSION_CREATED; + // A previous step failed. + if (!getConnectionStatus().hasPassed(ZoneMinderConnectionStatus.ZONEMINDER_SESSION_CREATED)) { + return; + } + + /*********************************** + * + * VALIDATE ZONEMINDER HTTP Session + * + ***********************************/ + setConnectionStatus(validateZoneMinderServerConfig()); + if (!getConnectionStatus().hasPassed(ZoneMinderConnectionStatus.ZONEMINDER_SERVER_CONFIG_PASSED)) { + return; + } + + /*********************************** + * + * Everything looks fine -> GO ONLINE + * + ***********************************/ - IZoneMinderSession curSession = null; + zmConnectStatus = ZoneMinderConnectionStatus.INITIALIZED; + // _zoneMinderSession = curSession; + newStatus = ThingStatus.ONLINE; + statusDetail = ThingStatusDetail.NONE; + statusDescription = ""; + + updateBridgeStatus(newStatus, statusDetail, statusDescription, true); + logger.debug("{}: context='{}' Successfully established session to ZoneMinder Server.", getLogIdentifier(), + context); + + } + + @Override + public void updateAvaliabilityStatus(IZoneMinderConnectionHandler conn) { + String context = "updateAvaliabilityStatus"; + IZoneMinderConnectionHandler curSession = null; + ThingStatus newStatus = ThingStatus.OFFLINE; + ThingStatusDetail statusDetail = ThingStatusDetail.NONE; + String statusDescription = ""; + + ThingStatus prevStatus = getThing().getStatus(); + try { + // Just perform a health check to see if we are still connected + if (prevStatus == ThingStatus.ONLINE) { try { - curSession = ZoneMinderFactory.CreateSession(connection); - } catch (FailedLoginException | IllegalArgumentException | IOException - | ZoneMinderUrlNotFoundException ex) { - logger.error("{}: Create Session failed with exception {}", getLogIdentifier(), ex.getMessage()); - - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; - statusDescription = "Failed to connect. (Check Log)"; - if (curBridgeStatus != ThingStatus.OFFLINE) { - logger.error("{}: Bridge OFFLINE because of '{}' Exception='{}'", getLogIdentifier(), - statusDescription, ex.getMessage()); + curSession = aquireSession(); + + if (curSession == null) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; + statusDescription = "Session lost connection to ZoneMinder Server"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + return; + } else if (!curSession.isConnected()) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; + statusDescription = "Session lost connection to ZoneMinder Server"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + return; } - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } - IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(curSession); - - // Check if server API can be accessed - if (!serverProxy.isApiEnabled()) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "ZoneMinder Server 'OPT_USE_API' not enabled"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } else if (!serverProxy.getHostDaemonCheckState().getStatus()) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "ZoneMinder Server Daemon not running"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; + else if (!curSession.isAuthenticated()) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; + statusDescription = "Not authenticated"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + return; + } + + IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(curSession); + IZoneMinderHostVersion hostVersion = null; + try { + hostVersion = serverProxy.getHostVersion(); + } catch (ZoneMinderException ex) { + hostVersion = null; + } + + if ((hostVersion == null) || (hostVersion.getHttpStatus() >= 400)) { + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; + statusDescription = "Connection to ZoneMinder Server was lost"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + + logger.error("{}: Lost connection to ZoneMinder server.", getLogIdentifier()); + + setConnected(false); + } + + // Check if ZoneMinder Server Daemon is running + if (!serverProxy.isDaemonRunning()) { + logger.error("{}: context='{}' Bridge OFFLINE because ZoneMinder Server Daemon stopped", + getLogIdentifier(), context); + zmConnectStatus = ZoneMinderConnectionStatus.SERVER_DAEMON_NOT_RUNNING; + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server Daemon stopped"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + // setConnected(false); + return; + } + // Verify that 'OPT_TRIGGER' is set to true in ZoneMinder + else if (!serverProxy.isTriggerOptionEnabled()) { + logger.error( + "{}: context='{}' Bridge OFFLINE because ZoneMinder Server OPT_TRIGGER was disabled", + getLogIdentifier(), context); + zmConnectStatus = ZoneMinderConnectionStatus.SERVER_OPT_TRIGGERS_DISABLED; + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "Option external triggers 'OPT_TRIGGERS' was disabled"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + return; + } + // Check if ZoneMinder Server API can be accessed + else if (!serverProxy.isApiEnabled()) { + logger.error("{}: context='{}' Bridge OFFLINE because ZoneMinder Server API was disabled", + getLogIdentifier(), context); + zmConnectStatus = ZoneMinderConnectionStatus.SERVER_API_DISABLED; + newStatus = ThingStatus.OFFLINE; + statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; + statusDescription = "ZoneMinder Server API was disabled"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, false); + return; + } + + } finally { + releaseSession(); } - // Verify that 'OPT_TRIGGER' is set to true in ZoneMinder - else if (!serverProxy.isTriggerOptionEnabled()) { - newStatus = ThingStatus.OFFLINE; - statusDetail = ThingStatusDetail.CONFIGURATION_ERROR; - statusDescription = "ZoneMinder Server option 'OPT_TRIGGERS' not enabled"; - updateBridgeStatus(newStatus, statusDetail, statusDescription); - return; - } else { - // Seems like everything is as we want it :-) - _isOnline = true; + + newStatus = ThingStatus.ONLINE; + statusDetail = ThingStatusDetail.NONE; + statusDescription = ""; + updateBridgeStatus(newStatus, statusDetail, statusDescription, true); + + // Ask all child things to update their Availability Status + for (Thing thing : getThing().getThings()) { + ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler(); + if (thingHandler instanceof ZoneMinderThingMonitorHandler) { + try { + thingHandler.updateAvaliabilityStatus(getZoneMinderConnection()); + } catch (Exception ex) { + logger.debug("{}: Failed to call 'updateAvailabilityStatus()' for '{}'", getLogIdentifier(), + thingHandler.getThing().getUID()); + } + } } - if (_isOnline == true) { - zoneMinderSession = curSession; - _online = _isOnline; + } else if (prevStatus == ThingStatus.OFFLINE) { + initializeAvaliabilityStatus(conn); + + if (getConnectionStatus() == ZoneMinderConnectionStatus.INITIALIZED) { newStatus = ThingStatus.ONLINE; statusDetail = ThingStatusDetail.NONE; statusDescription = ""; - - } else { - zoneMinderSession = null; - _online = _isOnline; - newStatus = ThingStatus.OFFLINE; + updateBridgeStatus(newStatus, statusDetail, statusDescription, true); } } - } catch (Exception ex) { + // Hmmm We shouldn't really end here. + logger.error( + "{}: context='updateAvailablilityStatus' Exception occurred in updateAvailabilityStatus Exception='{}'", + getLogIdentifier(), ex.getMessage(), ex.getCause()); + zmConnectStatus = ZoneMinderConnectionStatus.GENERAL_ERROR; newStatus = ThingStatus.OFFLINE; statusDetail = ThingStatusDetail.COMMUNICATION_ERROR; - logger.error("{}: Exception occurred in updateAvailabilityStatus Exception='{}'", getLogIdentifier(), - ex.getMessage()); - statusDescription = "Error occurred (Check log)"; - - } - updateBridgeStatus(newStatus, statusDetail, statusDescription); - - // Ask child things to update their Availability Status - for (Thing thing : getThing().getThings()) { - ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler(); - if (thingHandler instanceof ZoneMinderThingMonitorHandler) { - try { - thingHandler.updateAvaliabilityStatus(connection); - } catch (Exception ex) { - logger.debug("{}: Failed to call 'updateAvailabilityStatus()' for '{}'", getLogIdentifier(), - thingHandler.getThing().getUID()); - } - } - + statusDescription = "General error occurred (Check log)"; + updateBridgeStatus(newStatus, statusDetail, statusDescription, true); } } - protected void updateBridgeStatus(ThingStatus newStatus, ThingStatusDetail statusDetail, String statusDescription) { + protected void updateBridgeStatus(ThingStatus newStatus, ThingStatusDetail statusDetail, String statusDescription, + boolean updateConnection) { ThingStatusInfo curStatusInfo = thing.getStatusInfo(); String curDescription = StringUtils.isBlank(curStatusInfo.getDescription()) ? "" : curStatusInfo.getDescription(); // Status changed if ((curStatusInfo.getStatus() != newStatus) || (curStatusInfo.getStatusDetail() != statusDetail) - || (curDescription != statusDescription)) { - - // if (thing.getStatus() != newStatus) { - logger.info("{}: Bridge status changed from '{}' to '{}'", getLogIdentifier(), thing.getStatus(), - newStatus); - - if ((newStatus == ThingStatus.ONLINE) && (curStatusInfo.getStatus() != ThingStatus.ONLINE)) { - try { - setBridgeConnectionStatus(true); - onConnected(); - } catch (IllegalArgumentException e) { - // Just ignore that here - } - } else if ((newStatus == ThingStatus.OFFLINE) && (curStatusInfo.getStatus() != ThingStatus.OFFLINE)) { - try { - setBridgeConnectionStatus(false); - onDisconnected(); - } catch (IllegalArgumentException e) { - // Just ignore that here - } - + || (!curDescription.equals(statusDescription))) { + if (!curStatusInfo.getStatus().equals(newStatus)) { + logger.info("{}: context='updateBridgeStatus' Bridge status changed from '{}' to '{}'", + getLogIdentifier(), thing.getStatus(), newStatus); } + // Update Status correspondingly if ((newStatus == ThingStatus.OFFLINE) && (statusDetail != ThingStatusDetail.NONE)) { updateStatus(newStatus, statusDetail, statusDescription); + + forcedPriority = RefreshPriority.UNKNOWN; + if (updateConnection) { + try { + setConnected(false); + } catch (IllegalArgumentException | GeneralSecurityException | IOException + | ZoneMinderUrlNotFoundException e) { + logger.error( + "{}: context='updateBridgeStatus' Exception occurred when changing connected status", + getLogIdentifier(), e); + } + } } else { updateStatus(newStatus); + forcedPriority = RefreshPriority.PRIORITY_BATCH; + if (updateConnection) { + try { + setConnected(true); + } catch (IllegalArgumentException | GeneralSecurityException | IOException + | ZoneMinderUrlNotFoundException e) { + logger.error( + "{}: context='updateBridgeStatus' Exception occurred when changing connected status", + getLogIdentifier(), e); + } + } } - curBridgeStatus = newStatus; - } - } - - protected boolean isZoneMinderLoginValid(IZoneMinderConnectionInfo connection) { - try { - return ZoneMinderFactory.validateLogin(connection); - } catch (Exception e) { - return false; + // Ask all child things to update their Availability Status, since Bridge has changed + for (Thing thing : getThing().getThings()) { + ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler(); + if (thingHandler instanceof ZoneMinderThingMonitorHandler) { + try { + thingHandler.updateAvaliabilityStatus(getZoneMinderConnection()); + } catch (Exception ex) { + logger.debug( + "{}: context='updateBridgeStatus' Failed to call 'updateAvailabilityStatus' for '{}' (Exception='{}')", + getLogIdentifier(), thingHandler.getThing().getUID(), ex.getMessage()); + } + } + } } - } @Override public void updateChannel(ChannelUID channel) { State state = null; try { - switch (channel.getId()) { case ZoneMinderConstants.CHANNEL_ONLINE: updateState(channel, (isOnline() ? OnOffType.ON : OnOffType.OFF)); break; case ZoneMinderConstants.CHANNEL_SERVER_DISKUSAGE: - state = getServerDiskUsageState(); + if (getBridgeConfig().getDiskUsageRefresh() != RefreshPriority.DISABLED) { + state = getServerDiskUsageState(); + } else { + state = UnDefType.UNDEF; + } break; case ZoneMinderConstants.CHANNEL_SERVER_CPULOAD: @@ -901,58 +1305,91 @@ public void updateChannel(ChannelUID channel) { } if (state != null) { - logger.debug("{}: BridgeHandler.updateChannel(): Updating channel '{}' to state='{}'", - getLogIdentifier(), channel.getId(), state.toString()); updateState(channel.getId(), state); } } catch (Exception ex) { - - logger.error("{}: Error when 'updateChannel()' was called for thing='{}' (Exception='{}'", + logger.error( + "{}: context='updateChannel' Error when 'updateChannel()' was called for thing='{}' (Exception='{}'", getLogIdentifier(), channel.getId(), ex.getMessage()); + } + } + + public void subscribeMonitorEvents(ZoneMinderThingMonitorHandler monitorHandler) { + try { + if (zoneMinderEventSession != null) { + logger.info("{}: context='SubscribeMonitorEvents' thing='monitor' id='{}'", getLogIdentifier(), + monitorHandler.getZoneMinderId()); + + zoneMinderEventSession.subscribeMonitorEvents(zoneMinderConnection, monitorHandler.getZoneMinderId(), + monitorHandler); + } else { + logger.warn( + "{}: context='SubscribeMonitorEvents' thing='monitor' id='{}' - Could not subscribe to monitor events, because EventSession not initialisaed", + getLogIdentifier(), monitorHandler.getZoneMinderId()); + } + } catch (IllegalArgumentException | GeneralSecurityException | IOException | ZoneMinderUrlNotFoundException e) { + logger.error( + "{}: context='SubscribeMonitorEvents' - Exception occurred when subscribing for MonitorEvents. Exception='{}'", + getLogIdentifier(), e.getMessage()); } } - protected boolean openConnection() { - boolean connected = false; - if (isConnected() == false) { - logger.debug("{}: Connecting Bridge to ZoneMinder Server", getLogIdentifier()); + public void unsubscribeMonitorEvents(ZoneMinderThingMonitorHandler monitorHandler) { + try { + if (zoneMinderEventSession != null) { + zoneMinderEventSession.unsubscribeMonitorEvents(monitorHandler.getZoneMinderId(), monitorHandler); - try { - if (isConnected()) { - closeConnection(); - } - setConnected(connected); + logger.info("{}: context='UnsubscribeMonitorEvents' thing='monitor' id='{}'", getLogIdentifier(), + monitorHandler.getZoneMinderId()); - logger.info("{}: Connecting to ZoneMinder Server (result='{}'", getLogIdentifier(), connected); + } else { + logger.warn( + "{}: context='UnsubscribeMonitorEvents' thing='monitor' id='{}' - Could not unsubscribe to monitor events, because EventSession not initialisaed", + getLogIdentifier(), monitorHandler.getZoneMinderId()); + } + } catch (Exception ex) { + logger.error( + "{}: context='SubscribeMonitorEvents' - Exception occurred when subscribing for MonitorEvents.", + getLogIdentifier(), ex); + } - } catch (Exception exception) { - logger.error("{}: openConnection(): Exception: ", getLogIdentifier(), exception); - setConnected(false); - } finally { - if (isConnected() == false) { - closeConnection(); - } + } + + public void activateForceAlarm(String monitorId, Integer priority, String reason, String note, String showText, + Integer timeoutSeconds) { + try { + if (zoneMinderEventSession != null) { + zoneMinderEventSession.activateForceAlarm(monitorId, priority, reason, note, showText, timeoutSeconds); + } else { + logger.error("{}: context='activateForceAlarm' No EventSession active for Monitor with Id='{}'", + getLogIdentifier(), monitorId); } + } catch (IOException ex) { + logger.error("{}: context='activateForceAlarm' tag='exception' - Call to activeForceAlarm failed", + getLogIdentifier(), ex); } - return isConnected(); + } - synchronized void closeConnection() { + public void deactivateForceAlarm(String monitorId) { try { - logger.debug("{}: closeConnection(): Closed HTTP Connection!", getLogIdentifier()); - setConnected(false); + if (zoneMinderEventSession != null) { + zoneMinderEventSession.deactivateForceAlarm(monitorId); + } else { + logger.error("{}: context='deactivateForceAlarm' No EventSession active for Monitor with Id='{}'", + getLogIdentifier(), monitorId); + } - } catch (Exception exception) { - logger.error("{}: closeConnection(): Error closing connection - {}", getLogIdentifier(), - exception.getMessage()); + } catch (Exception ex) { + logger.error("{}: context='deactivateForceAlarm' tag='exception' - Call to deactiveForceAlarm failed", + getLogIdentifier(), ex); } } protected State getServerCpuLoadState() { - State state = UnDefType.UNDEF; try { @@ -962,14 +1399,13 @@ protected State getServerCpuLoadState() { } catch (Exception ex) { // Deliberately kept as debug info! - logger.debug("{}: Exception='{}'", getLogIdentifier(), ex.getMessage()); + logger.debug("{}: context='getServerCpuLoadState' Exception='{}'", getLogIdentifier(), ex.getMessage()); } return state; } protected State getServerDiskUsageState() { - State state = UnDefType.UNDEF; try { @@ -978,21 +1414,56 @@ protected State getServerDiskUsageState() { } } catch (Exception ex) { // Deliberately kept as debug info! - logger.debug("{}: Exception {}", getLogIdentifier(), ex.getMessage()); + logger.debug("{}: context='getServerDiskUsageState' Exception {}", getLogIdentifier(), ex.getMessage()); } return state; } @Override - public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionInfo connection) { - logger.info("{}: Brigde went ONLINE", getLogIdentifier()); + public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionHandler connection) { + IZoneMinderConnectionHandler session = null; + try { + session = aquireSession(); + + IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(session); + ZoneMinderConfig cfgPathZms = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_PATH_ZMS); + ZoneMinderConfig cfgOptFrameServer = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_FRAME_SERVER); + logger.debug("{}: context='onBridgeConnected' Api Enabled : {}", getLogIdentifier(), + zoneMinderConnection.isApiEnabled()); + logger.debug("{}: context='onBridgeConnected' Authentication Enabled : {}", getLogIdentifier(), + zoneMinderConnection.isAuthenticationEnabled()); + logger.debug("{}: context='onBridgeConnected' AuthHash Allowed : {}", getLogIdentifier(), + zoneMinderConnection.getAuthenticationHashAllowed()); + if (zoneMinderConnection.getAuthenticationHashAllowed()) { + logger.debug("{}: context='onBridgeConnected' AuthHash Relay : {}", getLogIdentifier(), + zoneMinderConnection.getAuthenticationHashReleayMethod().toString()); + } + logger.debug("{}: context='onBridgeConnected' Portal URI: {}", getLogIdentifier(), + zoneMinderConnection.getPortalUri().toString()); + logger.debug("{}: context='onBridgeConnected' API URI: {}", getLogIdentifier(), + zoneMinderConnection.getApiUri().toString()); + logger.debug("{}: context='onBridgeConnected' ZMS URI: {}", getLogIdentifier(), + cfgPathZms.getValueAsString()); + logger.debug("{}: context='onBridgeConnected' FrameServer: {}", getLogIdentifier(), + cfgOptFrameServer.getvalueAsBoolean()); + } catch (ZoneMinderException | Exception ex) { + logger.error( + "{}: context='onBridgeConnected' Exception occurred when calling 'onBridgeConencted()' Message='{}'", + getLogIdentifier(), ex.getMessage(), ex.getCause()); + + } finally { + if (session != null) { + releaseSession(); + } + } try { // Start the discovery service if (discoveryService == null) { discoveryService = new ZoneMinderDiscoveryService(this, 30); } + discoveryService.activate(); if (discoveryRegistration == null) { @@ -1001,45 +1472,29 @@ public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderC discoveryService, new Hashtable()); } } catch (Exception e) { - logger.error("BRIDGE [{}]: Exception occurred when starting discovery service Exception='{}'", getThingId(), - e.getMessage()); + logger.error("{}: context='onBridgeConnected' Exception occurred when starting discovery service", + getLogIdentifier(), e.getCause()); } - if (taskRefreshData == null) { - - // Perform first refresh manually (we want to force update of DiskUsage) - boolean updateDiskUsage = (getBridgeConfig().getRefreshIntervalLowPriorityTask() > 0) ? true : false; - refreshThing(zoneMinderSession, updateDiskUsage); - - if (getBridgeConfig().getRefreshIntervalLowPriorityTask() != 0) { - refreshFrequency = calculateCommonRefreshFrequency(getBridgeConfig().getRefreshInterval()); - } else { - refreshFrequency = getBridgeConfig().getRefreshInterval(); - } - logger.info("BRIDGE [{}]: Calculated refresh inetrval to '{}'", getThingId(), refreshFrequency); - - if (taskRefreshData != null) { - taskRefreshData.cancel(true); - taskRefreshData = null; - } - - // Start job to handle next updates - taskRefreshData = startTask(refreshDataRunnable, refreshFrequency, refreshFrequency, TimeUnit.SECONDS); + try { + // Update properties + updateServerProperties(); + } catch (Exception e) { + logger.error( + "{}: method='onBridgeConnected' context='updateServerProperties' Exception occurred when starting discovery service", + getLogIdentifier(), e.getCause()); - if (taskPriorityRefreshData != null) { - taskPriorityRefreshData.cancel(true); - taskPriorityRefreshData = null; - } + } - // Only start if Priority Frequency is higher than ordinary - if (refreshFrequency > 1) { - taskPriorityRefreshData = startTask(refreshPriorityDataRunnable, 0, 1, TimeUnit.SECONDS); - } + if (taskRefreshData != null) { + taskRefreshData.cancel(true); + taskRefreshData = null; } - // Update properties - updateMonitorProperties(zoneMinderSession); + // Start job to handle next updates + taskRefreshData = startTask(refreshDataRunnable, 1, 1, TimeUnit.SECONDS); + } @Override @@ -1056,13 +1511,6 @@ public void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge) { logger.debug("{}: Stopping DataRefresh task", getLogIdentifier()); } - // Stopping High priority thread while OFFLINE - if (taskPriorityRefreshData != null) { - taskPriorityRefreshData.cancel(true); - taskPriorityRefreshData = null; - logger.debug("{}: Stopping Priority DataRefresh task", getLogIdentifier()); - } - // Make sure everything gets refreshed for (Channel ch : getThing().getChannels()) { handleCommand(ch.getUID(), RefreshType.REFRESH); @@ -1085,7 +1533,7 @@ public void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge) { * Method to start a data refresh task. */ protected ScheduledFuture startTask(Runnable command, long delay, long interval, TimeUnit unit) { - logger.debug("BRIDGE [{}]: Starting ZoneMinder Bridge Monitor Task. Command='{}'", getThingId(), + logger.debug("{}: Starting ZoneMinder Bridge Monitor Task. Command='{}'", getLogIdentifier(), command.toString()); if (interval == 0) { return null; @@ -1106,56 +1554,69 @@ protected void stopTask(ScheduledFuture task) { } } catch (Exception ex) { } - } - public ArrayList getMonitors() { - if (isOnline()) { - - IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(zoneMinderSession); - ArrayList result = serverProxy.getMonitors(); + public ArrayList getMonitors() { + if (isConnected()) { + IZoneMinderServer serverProxy = null; + try { + serverProxy = ZoneMinderFactory.getServerProxy(aquireSession()); + ArrayList result = serverProxy.getMonitors(); + return result; + } catch (ZoneMinderGeneralException | ZoneMinderResponseException | ZoneMinderInvalidData + | ZoneMinderAuthenticationException ex) { + logger.error("{}: context='getMonitors' Exception occurred", getLogIdentifier(), ex.getCause()); - return result; + } finally { + if (serverProxy != null) { + releaseSession(); + } + } } - return new ArrayList(); + return new ArrayList<>(); } - /* - * This is experimental - * Try to add different properties - */ - private void updateMonitorProperties(IZoneMinderSession session) { + private void updateServerProperties() { + if (!isConnected()) { + return; + } + // Update property information about this device Map properties = editProperties(); - IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(session); + IZoneMinderConnectionHandler session = null; IZoneMinderHostVersion hostVersion = null; try { + session = aquireSession(); + IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(session); + hostVersion = serverProxy.getHostVersion(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - serverProxy.getHttpUrl(), serverProxy.getHttpResponseCode(), serverProxy.getHttpResponseMessage()); + if (hostVersion.getHttpStatus() != HttpStatus.OK_200) { + return; + } ZoneMinderConfig configUseApi = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_USE_API); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - serverProxy.getHttpUrl(), serverProxy.getHttpResponseCode(), serverProxy.getHttpResponseMessage()); ZoneMinderConfig configUseAuth = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_USE_AUTH); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - serverProxy.getHttpUrl(), serverProxy.getHttpResponseCode(), serverProxy.getHttpResponseMessage()); - ZoneMinderConfig configTrigerrs = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_TRIGGERS); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - configUseApi.getHttpUrl(), configUseApi.getHttpResponseCode(), - configUseApi.getHttpResponseMessage()); + ZoneMinderConfig configAllowHashLogin = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_AUTH_HASH_LOGINS); + ZoneMinderConfig configFrameServer = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_FRAME_SERVER); properties.put(ZoneMinderProperties.PROPERTY_SERVER_VERSION, hostVersion.getVersion()); properties.put(ZoneMinderProperties.PROPERTY_SERVER_API_VERSION, hostVersion.getApiVersion()); properties.put(ZoneMinderProperties.PROPERTY_SERVER_USE_API, configUseApi.getValueAsString()); properties.put(ZoneMinderProperties.PROPERTY_SERVER_USE_AUTHENTIFICATION, configUseAuth.getValueAsString()); + properties.put(ZoneMinderProperties.PROPERTY_SERVER_USE_AUTH_HASH, configAllowHashLogin.getValueAsString()); properties.put(ZoneMinderProperties.PROPERTY_SERVER_TRIGGERS_ENABLED, configTrigerrs.getValueAsString()); - } catch (FailedLoginException | ZoneMinderUrlNotFoundException | IOException e) { - logger.warn("{}: Exception occurred when updating monitor properties (Exception='{}'", getLogIdentifier(), - e.getMessage()); + properties.put(ZoneMinderProperties.PROPERTY_SERVER_FRAME_SERVER, configFrameServer.getValueAsString()); + + } catch (ZoneMinderUrlNotFoundException | IOException | ZoneMinderGeneralException | ZoneMinderResponseException + | ZoneMinderInvalidData | ZoneMinderAuthenticationException e) { + logger.warn("{}: Exception occurred when updating monitor properties", getLogIdentifier(), e); + } finally { + if (session != null) { + releaseSession(); + } } // Must loop over the new properties since we might have added data diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingMonitorHandler.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingMonitorHandler.java index 306ad59298796..0217b73920ab6 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingMonitorHandler.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingMonitorHandler.java @@ -8,19 +8,13 @@ */ package org.openhab.binding.zoneminder.handler; -import java.io.IOException; import java.math.BigDecimal; -import java.security.GeneralSecurityException; +import java.net.MalformedURLException; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.security.auth.login.FailedLoginException; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.thing.Bridge; -import org.eclipse.smarthome.core.thing.Channel; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; @@ -32,26 +26,36 @@ import org.eclipse.smarthome.core.types.UnDefType; import org.openhab.binding.zoneminder.ZoneMinderConstants; import org.openhab.binding.zoneminder.ZoneMinderProperties; -import org.openhab.binding.zoneminder.internal.DataRefreshPriorityEnum; +import org.openhab.binding.zoneminder.internal.RefreshPriority; import org.openhab.binding.zoneminder.internal.config.ZoneMinderThingMonitorConfig; +import org.openhab.binding.zoneminder.internal.state.ChannelStateChangeSubscriber; +import org.openhab.binding.zoneminder.internal.state.MonitorThingState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Sets; -import name.eskildsen.zoneminder.IZoneMinderConnectionInfo; -import name.eskildsen.zoneminder.IZoneMinderDaemonStatus; -import name.eskildsen.zoneminder.IZoneMinderEventData; +import name.eskildsen.zoneminder.IZoneMinderConnectionHandler; import name.eskildsen.zoneminder.IZoneMinderEventSubscriber; import name.eskildsen.zoneminder.IZoneMinderMonitor; -import name.eskildsen.zoneminder.IZoneMinderMonitorData; -import name.eskildsen.zoneminder.IZoneMinderSession; +import name.eskildsen.zoneminder.IZoneMinderServer; import name.eskildsen.zoneminder.ZoneMinderFactory; -import name.eskildsen.zoneminder.api.event.ZoneMinderEvent; +import name.eskildsen.zoneminder.api.monitor.ZoneMinderMonitorStatus; import name.eskildsen.zoneminder.api.telnet.ZoneMinderTriggerEvent; +import name.eskildsen.zoneminder.common.ZoneMinderConfigEnum; import name.eskildsen.zoneminder.common.ZoneMinderMonitorFunctionEnum; -import name.eskildsen.zoneminder.common.ZoneMinderMonitorStatusEnum; -import name.eskildsen.zoneminder.exception.ZoneMinderUrlNotFoundException; +import name.eskildsen.zoneminder.data.IMonitorDataGeneral; +import name.eskildsen.zoneminder.data.IMonitorDataStillImage; +import name.eskildsen.zoneminder.data.IZoneMinderDaemonStatus; +import name.eskildsen.zoneminder.data.IZoneMinderEventData; +import name.eskildsen.zoneminder.data.ZoneMinderConfig; +import name.eskildsen.zoneminder.exception.ZoneMinderAuthHashNotEnabled; +import name.eskildsen.zoneminder.exception.ZoneMinderAuthenticationException; +import name.eskildsen.zoneminder.exception.ZoneMinderException; +import name.eskildsen.zoneminder.exception.ZoneMinderGeneralException; +import name.eskildsen.zoneminder.exception.ZoneMinderInvalidData; +import name.eskildsen.zoneminder.exception.ZoneMinderResponseException; +import name.eskildsen.zoneminder.internal.ZoneMinderContentResponse; /** * The {@link ZoneMinderThingMonitorHandler} is responsible for handling commands, which are @@ -59,7 +63,8 @@ * * @author Martin S. Eskildsen - Initial contribution */ -public class ZoneMinderThingMonitorHandler extends ZoneMinderBaseThingHandler implements IZoneMinderEventSubscriber { +public class ZoneMinderThingMonitorHandler extends ZoneMinderBaseThingHandler + implements ChannelStateChangeSubscriber, IZoneMinderEventSubscriber { public static final Set SUPPORTED_THING_TYPES = Sets .newHashSet(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR); @@ -68,32 +73,19 @@ public class ZoneMinderThingMonitorHandler extends ZoneMinderBaseThingHandler im private static final int MAX_MONITOR_STATUS_WATCH_COUNT = 3; /** Make sure we can log errors, warnings or what ever somewhere */ - private Logger logger = LoggerFactory.getLogger(ZoneMinderThingMonitorHandler.class); + private final Logger logger = LoggerFactory.getLogger(ZoneMinderThingMonitorHandler.class); + private RefreshPriority forcedPriority = RefreshPriority.DISABLED; private String lastMonitorStatus = MONITOR_STATUS_NOT_INIT; private Integer monitorStatusMatchCount = 3; private ZoneMinderThingMonitorConfig config; - private Boolean _running = false; - - private ZoneMinderEvent curEvent = null; + MonitorThingState dataConverter = new MonitorThingState(this); - /** - * Channels - */ - private ZoneMinderMonitorFunctionEnum channelFunction = ZoneMinderMonitorFunctionEnum.NONE; - private Boolean channelEnabled = false; - private boolean channelRecordingState = false; - private boolean channelAlarmedState = false; - private String channelEventCause = ""; - private ZoneMinderMonitorStatusEnum channelMonitorStatus = ZoneMinderMonitorStatusEnum.UNKNOWN; - private boolean channelDaemonCapture = false; - private boolean channelDaemonAnalysis = false; - private boolean channelDaemonFrame = false; - private boolean channelForceAlarm = false; - - private int forceAlarmManualState = -1; + private long lastRefreshGeneralData = 0; + private long lastRefreshStillImage = 0; + private boolean frameDaemonActive = false; public ZoneMinderThingMonitorHandler(Thing thing) { super(thing); @@ -103,6 +95,15 @@ public ZoneMinderThingMonitorHandler(Thing thing) { @Override public void dispose() { + try { + ZoneMinderServerBridgeHandler bridge = getZoneMinderBridgeHandler(); + logger.info("{}: Unsubscribing from Monitor Events: {}", getLogIdentifier(), + bridge.getThing().getUID().getAsString()); + bridge.unsubscribeMonitorEvents(this); + + } catch (Exception ex) { + logger.error("{}: Exception occurred when calling 'onBridgeDisonnected()'.", getLogIdentifier(), ex); + } } @Override @@ -117,63 +118,119 @@ public String getZoneMinderId() { } @Override - public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionInfo connection) - throws IllegalArgumentException, GeneralSecurityException, IOException, ZoneMinderUrlNotFoundException { - + public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionHandler connection) { try { - logger.info("{}: Bridge '{}' connected", getLogIdentifier(), bridge.getThing().getUID().getAsString()); + logger.debug("{}: Bridge '{}' connected", getLogIdentifier(), bridge.getThing().getUID().getAsString()); super.onBridgeConnected(bridge, connection); - ZoneMinderFactory.SubscribeMonitorEvents(connection, config.getZoneMinderId(), this); - IZoneMinderSession session = aquireSession(); - IZoneMinderMonitor monitor = ZoneMinderFactory.getMonitorProxy(session, config.getZoneMinderId()); - IZoneMinderMonitorData monitorData = monitor.getMonitorData(); + logger.info("{}: Add subsription for Monitor Events: {}", getLogIdentifier(), + bridge.getThing().getUID().getAsString()); + bridge.subscribeMonitorEvents(this); + + IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(connection); + ZoneMinderConfig cfg = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_FRAME_SERVER); + frameDaemonActive = cfg.getvalueAsBoolean(); + } catch (ZoneMinderGeneralException | ZoneMinderResponseException | ZoneMinderAuthenticationException + | ZoneMinderInvalidData ex) { + logger.error("{}: context='onBridgeConnected' error in call to 'getServerProxy' - Message='{}'", + getLogIdentifier(), ex.getMessage(), ex.getCause()); + + } catch (MalformedURLException e) { + logger.error("{}: context='onBridgeConnected' error in call to 'getServerProxy' - Message='{}' (Exception)", + getLogIdentifier(), e.getMessage(), e.getCause()); + } - logger.debug("{}: SourceType: {}", getLogIdentifier(), monitorData.getSourceType().name()); - logger.debug("{}: Format: {}", getLogIdentifier(), monitorData.getFormat()); - logger.debug("{}: AlarmFrameCount: {}", getLogIdentifier(), monitorData.getAlarmFrameCount()); - logger.debug("{}: AlarmMaxFPS: {}", getLogIdentifier(), monitorData.getAlarmMaxFPS()); - logger.debug("{}: AnalysisFPS: {}", getLogIdentifier(), monitorData.getAnalysisFPS()); - logger.debug("{}: Height x Width: {} x {}", getLogIdentifier(), monitorData.getHeight(), - monitorData.getWidth()); + } - updateMonitorProperties(session); + @Override + public void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge) { + try { + logger.debug("{}: Bridge '{}' disconnected", getLogIdentifier(), bridge.getThing().getUID().getAsString()); - } catch (Exception ex) { - logger.error("{}: Exception occurred when calling 'onBridgeConencted()'. Exception='{}'", - getLogIdentifier(), ex.getMessage()); + super.onBridgeDisconnected(bridge); - } finally { - releaseSession(); + } catch (Exception ex) { + logger.error("{}: Exception occurred when calling 'onBridgeDisonencted()'.", getLogIdentifier(), ex); } } @Override - public void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge) { - try { - logger.info("{}: Bridge '{}' disconnected", getLogIdentifier(), bridge.getThing().getUID().getAsString()); + public void onThingStatusChanged(ThingStatus thingStatus) { + if (thingStatus == ThingStatus.ONLINE) { + IZoneMinderConnectionHandler connection = null; + try { + connection = aquireSessionWait(); + IZoneMinderMonitor monitor = ZoneMinderFactory.getMonitorProxy(connection, config.getZoneMinderId()); + IMonitorDataGeneral monitorData = monitor.getMonitorData(); + + logger.debug("{}: SourceType: {}", getLogIdentifier(), monitorData.getSourceType().name()); + logger.debug("{}: Format: {}", getLogIdentifier(), monitorData.getFormat()); + logger.debug("{}: AlarmFrameCount: {}", getLogIdentifier(), monitorData.getAlarmFrameCount()); + logger.debug("{}: AlarmMaxFPS: {}", getLogIdentifier(), monitorData.getAlarmMaxFPS()); + logger.debug("{}: AnalysisFPS: {}", getLogIdentifier(), monitorData.getAnalysisFPS()); + logger.debug("{}: Height x Width: {} x {}", getLogIdentifier(), monitorData.getHeight(), + monitorData.getWidth()); + } catch (ZoneMinderInvalidData | ZoneMinderAuthenticationException | ZoneMinderGeneralException + | ZoneMinderResponseException ex) { + logger.error("{}: context='onThingStatusChanged' error in call to 'getMonitorData' - Message='{}'", + getLogIdentifier(), ex.getMessage(), ex.getCause()); + + } finally { + if (connection != null) { + releaseSession(); + } + } - logger.info("{}: Unsubscribing from Monitor Events: {}", getLogIdentifier(), - bridge.getThing().getUID().getAsString()); - ZoneMinderFactory.UnsubscribeMonitorEvents(config.getZoneMinderId(), this); + try { + updateMonitorProperties(); - logger.debug("{}: Calling parent onBridgeConnected()", getLogIdentifier()); - super.onBridgeDisconnected(bridge); + } catch (Exception ex) { + logger.error( + "{}: context='onThingStatusChanged' - Exception occurred when calling 'updateMonitorPropoerties()'. Exception='{}'", + getLogIdentifier(), ex.getMessage()); + + } + } + } + @Override + public void channelLinked(ChannelUID channelUID) { + try { + if (!channelUID.getId().equals(ZoneMinderConstants.CHANNEL_ONLINE)) { + dataConverter.subscribe(channelUID); + } + super.channelLinked(channelUID); + + logger.info("{}: context='channelLinked' - Unlinking from channel '{}'", getLogIdentifier(), + channelUID.getAsString()); } catch (Exception ex) { - logger.error("{}: Exception occurred when calling 'onBridgeDisonencted()'. Exception='{}'", - getLogIdentifier(), ex.getMessage()); + logger.info("{}: context='channelUnlinked' - Exception when Unlinking from channel '{}' - EXCEPTION)'{}'", + getLogIdentifier(), channelUID.getAsString(), ex.getMessage()); } } + @Override + public void channelUnlinked(ChannelUID channelUID) { + try { + dataConverter.unsubscribe(channelUID); + super.channelUnlinked(channelUID); + logger.info("{}: context='channelUnlinked' - Unlinking from channel '{}'", getLogIdentifier(), + channelUID.getAsString()); + } catch (Exception ex) { + logger.info("{}: context='channelUnlinked' - Exception when Unlinking from channel '{}' - EXCEPTION)'{}'", + getLogIdentifier(), channelUID.getAsString(), ex.getMessage()); + + } + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { + IZoneMinderMonitor monitorProxy = null; try { - logger.debug("{}: Channel '{}' in monitor '{}' received command='{}'", getLogIdentifier(), channelUID, getZoneMinderId(), command); @@ -185,112 +242,193 @@ public void handleCommand(ChannelUID channelUID, Command command) { // Communication TO Monitor switch (channelUID.getId()) { - // Done via Telnet connection case ZoneMinderConstants.CHANNEL_MONITOR_FORCE_ALARM: - logger.debug( - "{}: 'handleCommand' => CHANNEL_MONITOR_FORCE_ALARM: Command '{}' received for monitor '{}'", - getLogIdentifier(), command, channelUID.getId()); - - if ((command == OnOffType.OFF) || (command == OnOffType.ON)) { - String eventText = getConfigValueAsString(ZoneMinderConstants.PARAMETER_MONITOR_EVENTTEXT); - - BigDecimal eventTimeout = getConfigValueAsBigDecimal( - ZoneMinderConstants.PARAMETER_MONITOR_TRIGGER_TIMEOUT); - - ZoneMinderServerBridgeHandler bridge = getZoneMinderBridgeHandler(); - if (bridge == null) { - logger.warn("'handleCommand()': Bridge is 'null'!"); - } - - IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), - getZoneMinderId()); - try { - if (command == OnOffType.ON) { - forceAlarmManualState = 1; - logger.info("{}: Activate 'ForceAlarm' to '{}' (Reason='{}', Timeout='{}')", - getLogIdentifier(), command, eventText, eventTimeout.intValue()); - - monitorProxy.activateForceAlarm(255, ZoneMinderConstants.MONITOR_EVENT_OPENHAB, - eventText, "", eventTimeout.intValue()); - - } - - else if (command == OnOffType.OFF) { - forceAlarmManualState = 0; - logger.info("{}: Cancel 'ForceAlarm'", getLogIdentifier()); - monitorProxy.deactivateForceAlarm(); + IZoneMinderConnectionHandler connection = null; + try { + // Force Alarm can only be activated when Function is either NODECT or MODECT + if ((dataConverter.getMonitorFunction() == ZoneMinderMonitorFunctionEnum.MODECT) + || (dataConverter.getMonitorFunction() == ZoneMinderMonitorFunctionEnum.NODECT)) { + logger.debug( + "{}: 'handleCommand' => CHANNEL_MONITOR_FORCE_ALARM: Command '{}' received for monitor '{}'", + getLogIdentifier(), command, channelUID.getId()); + + if ((command == OnOffType.OFF) || (command == OnOffType.ON)) { + dataConverter.setMonitorForceAlarmInternal((command == OnOffType.ON) ? true : false); + String eventText = getConfigValueAsString( + ZoneMinderConstants.PARAMETER_MONITOR_EVENTTEXT); + + BigDecimal eventTimeout = getConfigValueAsBigDecimal( + ZoneMinderConstants.PARAMETER_MONITOR_TRIGGER_TIMEOUT); + + try { + connection = aquireSession(); + if (connection == null) { + logger.error( + "{}: context='handleCommand' tags='ForceAlarm' - Command='{}' failed to obtain session", + getLogIdentifier(), command); + return; + } + monitorProxy = ZoneMinderFactory.getMonitorProxy(connection, getZoneMinderId()); + + if (command == OnOffType.ON) { + logger.info("{}: Activate 'ForceAlarm' to '{}' (Reason='{}', Timeout='{}')", + getLogIdentifier(), command, eventText, eventTimeout.intValue()); + + getZoneMinderBridgeHandler().activateForceAlarm(getZoneMinderId(), 255, + ZoneMinderConstants.MONITOR_EVENT_OPENHAB, eventText, "", + eventTimeout.intValue()); + + dataConverter.setMonitorForceAlarmInternal(true); + + // Force a refresh + startAlarmRefresh(eventTimeout.intValue()); + + } + + else if (command == OnOffType.OFF) { + logger.debug("{}: Cancel 'ForceAlarm'", getLogIdentifier()); + + getZoneMinderBridgeHandler().deactivateForceAlarm(getZoneMinderId()); + dataConverter.setMonitorForceAlarmInternal(false); + // Stop Alarm Refresh + forceStopAlarmRefresh(); + + } + fetchMonitorGeneralData(monitorProxy); + + } catch (Exception ex) { + logger.error( + "{}: Context='handleCommand' Channel='{}' EXCEPTION: Call to 'ForceAlarm' Command='{}' failed", + getLogIdentifier(), channelUID.getId(), command, ex); + } } + } else { + logger.warn( + "{}: context='handleCommand' tag='CHANNEL_MONITOR_FORCE_ALARM' is inactive when function is not 'MODECT' or 'NODECT'", + getLogIdentifier()); - } finally { + } + } catch (Exception ex) { + logger.error("{}: context='handleCommand' tag='CHANNEL_MONITOR_FORCE_ALARM'", + getLogIdentifier()); + } finally { + if (monitorProxy != null) { + monitorProxy = null; releaseSession(); } - RecalculateChannelStates(); - - handleCommand(channelUID, RefreshType.REFRESH); - handleCommand(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_STATE), - RefreshType.REFRESH); - handleCommand(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_RECORD_STATE), - RefreshType.REFRESH); - - // Force a refresh - startPriorityRefresh(); - + requestChannelRefresh(); } break; case ZoneMinderConstants.CHANNEL_MONITOR_ENABLED: - logger.debug( - "{}: 'handleCommand' => CHANNEL_MONITOR_ENABLED: Command '{}' received for monitor '{}'", - getLogIdentifier(), command, channelUID.getId()); - - if ((command == OnOffType.OFF) || (command == OnOffType.ON)) { - boolean newState = ((command == OnOffType.ON) ? true : false); - - IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), - getZoneMinderId()); - try { - monitorProxy.SetEnabled(newState); - } finally { - releaseSession(); - } + try { + logger.debug( + "{}:context='handleCommand' tag='CHANNEL_MONITOR_ENABLED' Command '{}' received for monitor '{}'", + getLogIdentifier(), command, channelUID.getId()); + + if ((command == OnOffType.OFF) || (command == OnOffType.ON)) { + boolean newState = ((command == OnOffType.ON) ? true : false); + + ZoneMinderContentResponse zmcr = null; + try { + monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), getZoneMinderId()); + if (monitorProxy == null) { + logger.error( + "{}: Connection to ZoneMinder Server was lost when handling command '{}'. Restart openHAB", + getLogIdentifier(), command); + return; + } + zmcr = monitorProxy.SetEnabled(newState); + logger.debug("{}: ResponseCode='{}' ResponseMessage='{}' URL='{}'", getLogIdentifier(), + zmcr.getHttpStatus(), zmcr.getHttpResponseMessage(), zmcr.getHttpRequestUrl()); + + } catch (ZoneMinderException ex) { + logger.error( + "{}: context='handleCommand' error in call to 'SetEnabled' ExceptionClass='{}' - Message='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex.getMessage(), + ex.getCause()); + } finally { + if (monitorProxy != null) { + monitorProxy = null; + releaseSession(); + } + } - channelEnabled = newState; + dataConverter.setMonitorEnabled(newState); - logger.info("{}: Setting enabled to '{}'", getLogIdentifier(), command); - } + logger.info( + "{}: context='handleCommand' tags='enabled' - Successfully changed function setting to '{}'", + getLogIdentifier(), command); + } + } catch (Exception ex) { + logger.error("{}: Exception in 'handleCommand' => 'CHANNEL_MONITOR_ENABLE' Exception='{}'", + getLogIdentifier(), ex.getMessage()); - handleCommand(channelUID, RefreshType.REFRESH); + } finally { + requestChannelRefresh(); + } break; case ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION: - String commandString = ""; - if (ZoneMinderMonitorFunctionEnum.isValid(command.toString())) { + try { + logger.debug( + "{}: context='handleCommand' tag='CHANNEL_MONITOR_FUNCTION' Command '{}' received for monitor '{}'", + getLogIdentifier(), command, channelUID.getId()); + + String commandString = ""; + if (ZoneMinderMonitorFunctionEnum.isValid(command.toString())) { + commandString = ZoneMinderMonitorFunctionEnum.getEnum(command.toString()).toString(); + ZoneMinderContentResponse zmcr = null; + try { + // Change Function for camera in ZoneMinder + monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), getZoneMinderId()); + if (monitorProxy == null) { + logger.error( + "{}: Connection to ZoneMinder Server was lost when handling command '{}'. Restart openHAB", + getLogIdentifier(), command); + return; + } + + zmcr = monitorProxy.SetFunction(commandString); + + logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), + zmcr.getHttpRequestUrl(), zmcr.getHttpStatus(), zmcr.getHttpResponseMessage()); + + fetchMonitorGeneralData(monitorProxy); + fetchMonitorDaemonStatus(true, true, monitorProxy); + + } catch (ZoneMinderAuthenticationException | ZoneMinderGeneralException + | ZoneMinderResponseException ex) { + } finally { + if (monitorProxy != null) { + monitorProxy = null; + releaseSession(); + } + } - commandString = ZoneMinderMonitorFunctionEnum.getEnum(command.toString()).toString(); - ZoneMinderServerBridgeHandler bridge = getZoneMinderBridgeHandler(); + dataConverter.setMonitorFunction(ZoneMinderMonitorFunctionEnum.getEnum(command.toString())); - IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), - getZoneMinderId()); - try { - monitorProxy.SetFunction(commandString); - } finally { - releaseSession(); - } + logger.debug( + "{}: context='handleCommand' tags='function' - Successfully changed function setting to '{}'", + getLogIdentifier(), commandString); - // Make sure local copy is set to new value - channelFunction = ZoneMinderMonitorFunctionEnum.getEnum(command.toString()); + } else { + logger.error( + "{}: Value '{}' for monitor channel is not valid. Accepted values is: 'None', 'Monitor', 'Modect', Record', 'Mocord', 'Nodect'", + getLogIdentifier(), commandString); + } + } catch (Exception ex) { + logger.error("{}: Exception in 'handleCommand' => 'CHANNEL_MONITOR_FUNCTION'", + getLogIdentifier(), ex.getCause()); - logger.info("{}: Setting function to '{}'", getLogIdentifier(), commandString); + } finally { + requestChannelRefresh(); - } else { - logger.error( - "{}: Value '{}' for monitor channel is not valid. Accepted values is: 'None', 'Monitor', 'Modect', Record', 'Mocord', 'Nodect'", - getLogIdentifier(), commandString); } - handleCommand(channelUID, RefreshType.REFRESH); + break; // They are all readonly in the channel config. @@ -310,19 +448,28 @@ else if (command == OnOffType.OFF) { break; } } catch (Exception ex) { - logger.error("{}: handleCommand: Command='{}' failed for channel='{}' Exception='{}'", getLogIdentifier(), - command, channelUID.getId(), ex.getMessage()); + logger.error("{}: handleCommand: Command='{}' failed for channel='{}'", getLogIdentifier(), command, + channelUID.getId(), ex); } } @Override public void initialize() { - try { - super.initialize(); this.config = getMonitorConfig(); - logger.info("{}: ZoneMinder Monitor Handler Initialized", getLogIdentifier()); - logger.debug("{}: Monitor Id: {}", getLogIdentifier(), config.getZoneMinderId()); + + super.initialize(); + logger.info("{}: context='initialize' Monitor Handler Initialized", getLogIdentifier()); + + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_FORCE_ALARM)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_RECORD_STATE)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_MOTION_EVENT)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_ENABLED)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION)); + dataConverter.addChannel(getChannelUIDFromChannelId(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_STATE)); + } catch (Exception ex) { logger.error("{}: Exception occurred when calling 'initialize()'. Exception='{}'", getLogIdentifier(), ex.getMessage()); @@ -332,24 +479,73 @@ public void initialize() { @Override public void onTrippedForceAlarm(ZoneMinderTriggerEvent event) { try { - logger.info("{}: Received forceAlarm for monitor {}", getLogIdentifier(), event.getMonitorId()); - Channel channel = this.getThing().getChannel(ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS); - Channel chEventCause = this.getThing().getChannel(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE); + logger.debug("{}: context='onTrippedForceAlarm' Received forceAlarm for monitor {}", getLogIdentifier(), + event.getMonitorId()); + + if (!isThingOnline()) { + logger.warn("{}: context='onTrippedForceAlarm' Skipping event '{}', because Thing is 'OFFLINE'", + getLogIdentifier(), event.toString()); + return; + } + + IZoneMinderEventData eventData = null; // Set Current Event to actual event if (event.getState()) { - startPriorityRefresh(); + IZoneMinderConnectionHandler connection = null; + try { + connection = aquireSession(); + IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(connection, getZoneMinderId()); + eventData = monitorProxy.getEventById(event.getEventId()); + + logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), + eventData.getHttpRequestUrl(), eventData.getHttpStatus(), + eventData.getHttpResponseMessage()); + + } catch (Exception ex) { + logger.error( + "{}: context='onTrippedForceAlarm' tag='session' Exception occurred when aquiring session - Exception='{}'", + getLogIdentifier(), ex.getMessage()); + + } catch (ZoneMinderInvalidData | ZoneMinderAuthenticationException | ZoneMinderGeneralException + | ZoneMinderResponseException ex) { + logger.error( + "{}: context='onTrippedForceAlarm' error in call to 'getMonitorProxy' ExceptionClass='{}' - Message='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex.getMessage(), ex.getCause()); + } finally { + if (connection != null) { + releaseSession(); + } + + } + dataConverter.disableRefresh(); + dataConverter.setMonitorForceAlarmExternal(event.getState()); + dataConverter.setMonitorEventData(eventData); + dataConverter.enableRefresh(); + + forceStartAlarmRefresh(); } else { - curEvent = null; + dataConverter.disableRefresh(); + + dataConverter.setMonitorForceAlarmExternal(event.getState()); + dataConverter.setMonitorEventData(null); + dataConverter.enableRefresh(); + forceStopAlarmRefresh(); } + } catch (Exception ex) { - logger.error("{}: Exception occurred inTrippedForceAlarm() Exception='{}'", getLogIdentifier(), - ex.getMessage()); + logger.error("{}: context='onTrippedForceAlarm' Exception occurred inTrippedForceAlarm() Exception='{}'", + getLogIdentifier(), ex.getMessage()); } } + @Override + protected void updateState(ChannelUID channelUID, State state) { + super.updateState(channelUID, state); + } + protected ZoneMinderThingMonitorConfig getMonitorConfig() { return this.getConfigAs(ZoneMinderThingMonitorConfig.class); } @@ -359,41 +555,9 @@ protected String getZoneMinderThingType() { return ZoneMinderConstants.THING_ZONEMINDER_MONITOR; } - private Boolean isDaemonRunning(Boolean daemonStatus, String daemonStatusText) { - Boolean result = false; - - Pattern pattern = Pattern - .compile("[0-9]{2}/[0-9]{2}/[0-9]{2}\\s+([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"); - - Matcher matcher = pattern.matcher(daemonStatusText); - - if (matcher.find()) { - - String currentMonitorStatus = daemonStatusText.substring(matcher.start(), matcher.end()); - if (lastMonitorStatus.equals(currentMonitorStatus)) { - monitorStatusMatchCount++; - } else if (lastMonitorStatus.equals(MONITOR_STATUS_NOT_INIT)) { - // We have just started, so we will assume that the monitor is running (don't set match count - // to Zero) - monitorStatusMatchCount++; - lastMonitorStatus = daemonStatusText.substring(matcher.start(), matcher.end()); - } else { - monitorStatusMatchCount = 0; - lastMonitorStatus = daemonStatusText.substring(matcher.start(), matcher.end()); - } - } - - else { - monitorStatusMatchCount = 0; - lastMonitorStatus = ""; - logger.debug("MONITOR-{}: Online(): No match found in status text.", getLogIdentifier()); - } - return daemonStatus; - } - @Override - public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { - // Assume succes + public void updateAvaliabilityStatus(IZoneMinderConnectionHandler connection) { + // Assume success ThingStatus newThingStatus = ThingStatus.ONLINE; ThingStatusDetail thingStatusDetailed = ThingStatusDetail.NONE; String thingStatusDescription = ""; @@ -401,20 +565,9 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { ThingStatus curThingStatus = this.getThing().getStatus(); boolean connectionStatus = false; + // Is connected to ZoneMinder and thing is ONLINE if (isConnected() && curThingStatus == ThingStatus.ONLINE) { - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); - return; - } - - try { - connectionStatus = ZoneMinderFactory.validateConnection(connection); - } catch (IllegalArgumentException e) { - logger.error("{}: validateConnection failed with exception='{}'", getLogIdentifier(), e.getMessage()); - newThingStatus = ThingStatus.OFFLINE; - thingStatusDetailed = ThingStatusDetail.COMMUNICATION_ERROR; - thingStatusDescription = "Could not connect to thing"; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); return; } @@ -425,14 +578,14 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { // 1. Is there a Bridge assigned? if (getBridge() == null) { msg = String.format("No Bridge assigned to monitor '%s'", thing.getUID()); - logger.error("{}: {}", getLogIdentifier(), msg); + logger.error("{}: context='updateAvailabilityStatus' {}", getLogIdentifier(), msg); newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.BRIDGE_OFFLINE; thingStatusDescription = "No Bridge assigned to monitor"; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); return; } else { - logger.debug("{}: ThingAvailability: Thing '{}' has Bridge '{}' defined (Check PASSED)", + logger.debug( + "{}: context='updateAvailabilityStatus' ThingAvailability: Thing '{}' has Bridge '{}' defined (Check PASSED)", getLogIdentifier(), thing.getUID(), getBridge().getBridgeUID()); } @@ -442,12 +595,12 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.BRIDGE_OFFLINE; thingStatusDescription = msg; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); - logger.error("{}: {}", getLogIdentifier(), msg); + logger.error("{}: context='updateAvailabilityStatus' {}", getLogIdentifier(), msg); return; } else { - logger.debug("{}: ThingAvailability: Bridge '{}' is ONLINE (Check PASSED)", getLogIdentifier(), - getBridge().getBridgeUID()); + logger.debug( + "{}: context='updateAvailabilityStatus' ThingAvailability: Bridge '{}' is ONLINE (Check PASSED)", + getLogIdentifier(), getBridge().getBridgeUID()); } // 3. Is Configuration OK? @@ -456,12 +609,12 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.CONFIGURATION_ERROR; thingStatusDescription = msg; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); - logger.error("{}: {}", getLogIdentifier(), msg); + logger.error("{}: context='updateAvailabilityStatus' {}", getLogIdentifier(), msg); return; } else { - logger.debug("{}: ThingAvailability: Thing '{}' has valid configuration (Check PASSED)", + logger.debug( + "{}: context='updateAvailabilityStatus' ThingAvailability: Thing '{}' has valid configuration (Check PASSED)", getLogIdentifier(), thing.getUID()); } @@ -471,39 +624,27 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.CONFIGURATION_ERROR; thingStatusDescription = msg; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); logger.error("{}: {}", getLogIdentifier(), msg); return; } else { - logger.debug("{}: ThingAvailability: ZoneMinder Id for Thing '{}' defined (Check PASSED)", + logger.debug( + "{}: context='updateAvailabilityStatus' ThingAvailability: ZoneMinder Id for Thing '{}' defined (Check PASSED)", getLogIdentifier(), thing.getUID()); } IZoneMinderMonitor monitorProxy = null; IZoneMinderDaemonStatus captureDaemon = null; - // TODO:: Also look at Analysis and Frame Daemons (only if they are supposed to be running) - // IZoneMinderSession session = aquireSession(); - - IZoneMinderSession curSession = null; - try { - curSession = ZoneMinderFactory.CreateSession(connection); - } catch (FailedLoginException | IllegalArgumentException | IOException - | ZoneMinderUrlNotFoundException ex) { - logger.error("{}: Create Session failed with exception {}", getLogIdentifier(), ex.getMessage()); - - newThingStatus = ThingStatus.OFFLINE; - thingStatusDetailed = ThingStatusDetail.COMMUNICATION_ERROR; - thingStatusDescription = "Failed to connect. (Check Log)"; - - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); - return; - } - + // Consider also looking at Analysis and Frame Daemons (only if they are supposed to be running) + IZoneMinderConnectionHandler curSession = connection; if (curSession != null) { monitorProxy = ZoneMinderFactory.getMonitorProxy(curSession, getZoneMinderId()); captureDaemon = monitorProxy.getCaptureDaemonStatus(); + logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), + captureDaemon.getHttpRequestUrl(), captureDaemon.getHttpStatus(), + captureDaemon.getHttpResponseMessage()); + } if (captureDaemon == null) { @@ -511,7 +652,6 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.COMMUNICATION_ERROR; thingStatusDescription = msg; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); logger.error("{}: {}", getLogIdentifier(), msg); return; } else if (!captureDaemon.getStatus()) { @@ -519,25 +659,24 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.COMMUNICATION_ERROR; thingStatusDescription = msg; - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); logger.error("{}: {}", getLogIdentifier(), msg); return; } newThingStatus = ThingStatus.ONLINE; - - } catch (Exception exception) { + forcedPriority = RefreshPriority.PRIORITY_BATCH; + } catch (ZoneMinderException | Exception exception) { newThingStatus = ThingStatus.OFFLINE; thingStatusDetailed = ThingStatusDetail.COMMUNICATION_ERROR; thingStatusDescription = "Error occurred (Check log)"; updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); - logger.error("{}: 'ThingMonitorHandler.updateAvailabilityStatus()': Exception occurred '{}'", - getLogIdentifier(), exception.getMessage()); + logger.error("{}: context='updateAvailabilityStatus' Exception occurred '{}'", getLogIdentifier(), + exception.getMessage()); return; + } finally { + updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); } - - updateThingStatus(newThingStatus, thingStatusDetailed, thingStatusDescription); } /* @@ -549,53 +688,32 @@ public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) { */ @Override public void updateChannel(ChannelUID channel) { - State state = null; + State state = UnDefType.UNDEF; try { switch (channel.getId()) { - case ZoneMinderConstants.CHANNEL_MONITOR_ENABLED: - state = getChannelBoolAsOnOffState(channelEnabled); - break; - case ZoneMinderConstants.CHANNEL_ONLINE: - // Ask super class to handle, because this channel is shared for all things super.updateChannel(channel); - break; + return; + + case ZoneMinderConstants.CHANNEL_MONITOR_ENABLED: case ZoneMinderConstants.CHANNEL_MONITOR_FORCE_ALARM: - state = getChannelBoolAsOnOffState(channelForceAlarm); - break; case ZoneMinderConstants.CHANNEL_MONITOR_EVENT_STATE: - state = getChannelBoolAsOnOffState(channelAlarmedState); - break; - case ZoneMinderConstants.CHANNEL_MONITOR_RECORD_STATE: - state = getChannelBoolAsOnOffState(channelRecordingState); - break; - + case ZoneMinderConstants.CHANNEL_MONITOR_MOTION_EVENT: case ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS: - state = getDetailedStatus(); - break; - case ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE: - state = getChannelStringAsStringState(channelEventCause); - break; - case ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION: - state = getChannelStringAsStringState(channelFunction.toString()); - break; - case ZoneMinderConstants.CHANNEL_MONITOR_CAPTURE_DAEMON_STATE: - state = getChannelBoolAsOnOffState(channelDaemonCapture); - break; - case ZoneMinderConstants.CHANNEL_MONITOR_ANALYSIS_DAEMON_STATE: - state = getChannelBoolAsOnOffState(channelDaemonAnalysis); - break; - case ZoneMinderConstants.CHANNEL_MONITOR_FRAME_DAEMON_STATE: - state = getChannelBoolAsOnOffState(channelDaemonFrame); + case ZoneMinderConstants.CHANNEL_MONITOR_STILL_IMAGE: + state = null; break; + case ZoneMinderConstants.CHANNEL_MONITOR_VIDEOURL: + state = dataConverter.getVideoUrl(); + break; default: logger.warn("{}: updateChannel(): Monitor '{}': No handler defined for channel='{}'", getLogIdentifier(), thing.getLabel(), channel.getAsString()); @@ -605,14 +723,11 @@ public void updateChannel(ChannelUID channel) { } if (state != null) { - - logger.debug("{}: Setting channel '{}' to '{}'", getLogIdentifier(), channel.toString(), - state.toString()); updateState(channel.getId(), state); } } catch (Exception ex) { - logger.error("{}: Error when 'updateChannel' was called (channelId='{}'state='{}', exception'{}')", - getLogIdentifier(), channel.toString(), state.toString(), ex.getMessage()); + logger.error("{}: context='updateChannel' Error when updating channelId='{}' state='{}'", + getLogIdentifier(), channel.toString(), state.toString(), ex); } } @@ -624,265 +739,451 @@ public void updateStatus(ThingStatus status) { } - protected void RecalculateChannelStates() { - boolean recordingFunction = false; - boolean recordingDetailedState = false; - boolean alarmedFunction = false; - boolean alarmedDetailedState = false; - - // Calculate based on state of Function - switch (channelFunction) { - case NONE: - case MONITOR: - alarmedFunction = false; - recordingFunction = false; - break; - - case MODECT: - alarmedFunction = true; - recordingFunction = true; - break; - case RECORD: - alarmedFunction = false; - recordingFunction = true; - break; - case MOCORD: - alarmedFunction = true; - recordingFunction = true; - break; - case NODECT: - alarmedFunction = false; - recordingFunction = true; - break; - default: - recordingFunction = (curEvent != null) ? true : false; - } - logger.debug( - "{}: Recalculate channel states based on Function: Function='{}' -> alarmState='{}', recordingState='{}'", - getLogIdentifier(), channelFunction.name(), alarmedFunction, recordingFunction); - - // Calculated based on detailed Monitor Status - switch (channelMonitorStatus) { - case IDLE: - alarmedDetailedState = false; - recordingDetailedState = false; - channelForceAlarm = false; - channelEventCause = ""; - break; + private long getLastRefreshGeneralData() { + return lastRefreshGeneralData; + } - case PRE_ALARM: - alarmedDetailedState = true; - recordingDetailedState = true; - channelForceAlarm = false; - break; + private long getLastRefreshStillImage() { + return lastRefreshStillImage; + } - case ALARM: - alarmedDetailedState = true; - recordingDetailedState = true; - channelForceAlarm = true; - break; + private boolean refreshGeneralData() { + long now = System.currentTimeMillis(); + long lastUpdate = getLastRefreshGeneralData(); - case ALERT: - alarmedDetailedState = true; - recordingDetailedState = true; - channelForceAlarm = false; - break; + // Normal refresh interval + int interval = 10000; - case RECORDING: - alarmedDetailedState = false; - recordingDetailedState = true; - channelForceAlarm = false; - break; + if (!isInitialized()) { + return true; } - logger.debug( - "{}: Recalculate channel states based on Detailed State: DetailedState='{}' -> alarmState='{}', recordingState='{}'", - getLogIdentifier(), channelMonitorStatus.name(), alarmedDetailedState, recordingDetailedState); + if (dataConverter.isAlarmed()) { + // Alarm refresh interval + interval = 1000; + } + return ((now - lastUpdate) > interval) ? true : false; + } - // Check if Force alarm was initialed from openHAB - if (forceAlarmManualState == 0) { - if (channelForceAlarm) { - channelForceAlarm = false; - } else { - forceAlarmManualState = -1; - } - } else if (forceAlarmManualState == 1) { + private boolean refreshStillImage() { + RefreshPriority priority; + long now = System.currentTimeMillis(); + long lastUpdate = getLastRefreshStillImage(); - if (channelForceAlarm == false) { - channelForceAlarm = true; - } else { - forceAlarmManualState = -1; - } + // Normal refresh interval + int interval = 10000; + if (!isInitialized()) { + return true; + } + if (dataConverter.isAlarmed()) { + priority = getMonitorConfig().getImageRefreshEvent(); + } else { + priority = getMonitorConfig().getImageRefreshIdle(); } + switch (priority) { + case DISABLED: + return false; + + case PRIORITY_BATCH: + interval = 1000 * 60 * 60; + break; + + case PRIORITY_LOW: + interval = 1000 * 60; + break; + + case PRIORITY_NORMAL: + interval = 1000 * 10; + break; - // Now we can conclude on the Alarmed and Recording channel state - channelRecordingState = (recordingFunction && recordingDetailedState && channelEnabled); - channelAlarmedState = (alarmedFunction && alarmedDetailedState && channelEnabled); + case PRIORITY_HIGH: + interval = 1000 * 5; + break; + case PRIORITY_ALARM: + interval = 1000; + break; + default: + return false; + } + return ((now - lastUpdate) > interval) ? true : false; } @Override - protected void onFetchData() { + protected void onFetchData(RefreshPriority cyclePriority) { + IZoneMinderConnectionHandler session = null; + IMonitorDataGeneral data = null; - IZoneMinderSession session = null; + boolean refreshChannels = false; - session = aquireSession(); - try { - IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(session, getZoneMinderId()); + if (getThing().getStatus() != ThingStatus.ONLINE) { + return; + } - IZoneMinderMonitorData data = null; - IZoneMinderDaemonStatus captureDaemon = null; - IZoneMinderDaemonStatus analysisDaemon = null; - IZoneMinderDaemonStatus frameDaemon = null; + RefreshPriority curRefreshPriority = RefreshPriority.DISABLED; - data = monitorProxy.getMonitorData(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - monitorProxy.getHttpUrl(), monitorProxy.getHttpResponseCode(), - monitorProxy.getHttpResponseMessage()); + if (forcedPriority == RefreshPriority.UNKNOWN) { + return; + } - captureDaemon = monitorProxy.getCaptureDaemonStatus(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - monitorProxy.getHttpUrl(), monitorProxy.getHttpResponseCode(), - monitorProxy.getHttpResponseMessage()); + if (forcedPriority == RefreshPriority.DISABLED) { + curRefreshPriority = cyclePriority; + } else { + curRefreshPriority = forcedPriority; + forcedPriority = RefreshPriority.DISABLED; + } - analysisDaemon = monitorProxy.getAnalysisDaemonStatus(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - monitorProxy.getHttpUrl(), monitorProxy.getHttpResponseCode(), - monitorProxy.getHttpResponseMessage()); + session = null; + try { + session = aquireSession(); + if (session == null) { + logger.warn("{}: Failed to aquire session for refresh, refresh loop for monitor will be skipped.", + getLogIdentifier()); + return; + } - frameDaemon = monitorProxy.getFrameDaemonStatus(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - monitorProxy.getHttpUrl(), monitorProxy.getHttpResponseCode(), - monitorProxy.getHttpResponseMessage()); + } catch (Exception ex) { + logger.error("{}: Exception occurred when aquiring exception. Refresh loop for monitor will be skipped.", + getLogIdentifier(), ex.getCause()); + return; + } - if ((data.getHttpResponseCode() != 200) || (captureDaemon.getHttpResponseCode() != 200) - || (analysisDaemon.getHttpResponseCode() != 200) || (frameDaemon.getHttpResponseCode() != 200)) { + IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(session, getZoneMinderId()); + dataConverter.disableRefresh(); + /************************** + * + * Perform refresh of monitor data + **************************/ - if (data.getHttpResponseCode() != 200) { - logger.warn("{}: HTTP Response MonitorData: Code='{}', Message'{}'", getLogIdentifier(), - data.getHttpResponseCode(), data.getHttpResponseMessage()); + if (refreshGeneralData()) { + refreshChannels = true; + fetchMonitorGeneralData(monitorProxy); - channelMonitorStatus = ZoneMinderMonitorStatusEnum.UNKNOWN; - channelFunction = ZoneMinderMonitorFunctionEnum.NONE; - channelEnabled = false; - channelEventCause = ""; - } - if (captureDaemon.getHttpResponseCode() != 200) { - channelDaemonCapture = false; - logger.warn("{}: HTTP Response CaptureDaemon: Code='{}', Message'{}'", getLogIdentifier(), - captureDaemon.getHttpResponseCode(), captureDaemon.getHttpResponseMessage()); + fetchMonitorDaemonStatus(true, true, monitorProxy); + } + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_STILL_IMAGE)) { + try { + if (refreshStillImage()) { + lastRefreshStillImage = System.currentTimeMillis(); + IMonitorDataStillImage monitorImage = monitorProxy.getMonitorStillImage(config.getImageScale(), + 1000, null); + logger.debug("{}: context='onFetchData' tag='image' URL='{}' ResponseCode='{}'", getLogIdentifier(), + monitorImage.getHttpRequestUrl(), monitorImage.getHttpStatus()); + + dataConverter.setMonitorStillImage(monitorImage.getImage()); } - if (analysisDaemon.getHttpResponseCode() != 200) { - channelDaemonAnalysis = false; + } catch (MalformedURLException mue) { + logger.error( + "{}: context='onFetchData' NalformedURL Exception occurred when calling to 'getMonitorStillImage'", + getLogIdentifier(), mue.getCause()); + dataConverter.setMonitorStillImage(null); + } catch (Exception ex) { + logger.error("{}: context='onFetchData' error in call to 'getMonitorStillImage'", getLogIdentifier(), + ex.getCause()); + dataConverter.setMonitorStillImage(null); + } catch (ZoneMinderException ex) { + logger.error( + "{}: context='onFetchData' error in call to 'getMonitorStillImage' ExceptionClass='{}' - Message='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex.getMessage(), ex.getCause()); + } + } else { + dataConverter.setMonitorStillImage(null); - logger.warn("{}: HTTP Response AnalysisDaemon: Code='{}', Message='{}'", getLogIdentifier(), - analysisDaemon.getHttpResponseCode(), analysisDaemon.getHttpResponseMessage()); - } - if (frameDaemon.getHttpResponseCode() != 200) { - channelDaemonFrame = false; - logger.warn("{}: HTTP Response MonitorData: Code='{}', Message'{}'", getLogIdentifier(), - frameDaemon.getHttpResponseCode(), frameDaemon.getHttpResponseMessage()); - } + } - } else { - if (isConnected()) { - channelMonitorStatus = monitorProxy.getMonitorDetailedStatus(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - monitorProxy.getHttpUrl(), monitorProxy.getHttpResponseCode(), - monitorProxy.getHttpResponseMessage()); - - channelFunction = data.getFunction(); - channelEnabled = data.getEnabled(); - IZoneMinderEventData event = monitorProxy.getLastEvent(); - if (event != null) { - channelEventCause = event.getCause(); - } else { - channelEventCause = ""; - } + if (curRefreshPriority.isPriorityActive(RefreshPriority.PRIORITY_LOW)) - channelDaemonCapture = captureDaemon.getStatus(); - channelDaemonAnalysis = analysisDaemon.getStatus(); - channelDaemonFrame = frameDaemon.getStatus(); - } else { - channelMonitorStatus = ZoneMinderMonitorStatusEnum.UNKNOWN; - channelFunction = ZoneMinderMonitorFunctionEnum.NONE; - channelEnabled = false; - channelEventCause = ""; - channelDaemonCapture = false; - channelDaemonAnalysis = false; - channelDaemonFrame = false; + { + try { + if (dataConverter != null) { + String str = monitorProxy.getMonitorStreamingPath(config.getImageScale(), 1000, null); + dataConverter.setMonitorVideoUrl(str); } + } catch (MalformedURLException e1) { + logger.error("{}: MalformedURLException occurred when calling 'getMonitorStreamingPath()'", + getLogIdentifier(), e1.getCause()); + + } catch (ZoneMinderGeneralException zmge) { + logger.error( + "{}: context='onFetchData' error in call to 'getMonitorStreamingPath' Exception='{}', Message='{}", + getLogIdentifier(), zmge.getClass().getCanonicalName(), zmge.getMessage(), zmge.getCause()); + } catch (ZoneMinderResponseException zmre) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getMonitorStreamingPath' Exception='{}', Message='{} - Http: Status='{}', Mesage='{}'", + getLogIdentifier(), zmre.getClass().getCanonicalName(), zmre.getMessage(), zmre.getHttpStatus(), + zmre.getHttpMessage(), zmre.getCause()); + } catch (ZoneMinderAuthHashNotEnabled zmahne) { + logger.error( + "{}: context='onFetchData' error in call to 'getMonitorStreamingPath' Exception='{}', Message='{}'", + getLogIdentifier(), zmahne.getClass().getCanonicalName(), zmahne.getMessage(), + zmahne.getCause()); } - } finally { + } + + if (session != null) { releaseSession(); + session = null; } + dataConverter.enableRefresh(); + if (refreshChannels) { + logger.debug("{}: context='onFetchData' - Data has changed, channels need refreshing", getLogIdentifier()); + requestChannelRefresh(); + } + tryStopAlarmRefresh(); + } - RecalculateChannelStates(); + void fetchMonitorGeneralData(IZoneMinderMonitor proxy) { + IZoneMinderMonitor monitorProxy = proxy; + boolean doRelase = false; + if (monitorProxy == null) { + doRelase = true; + monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), getZoneMinderId()); + } - if ((channelForceAlarm == false) && (channelAlarmedState == false) - && (DataRefreshPriorityEnum.HIGH_PRIORITY == getRefreshPriority())) { - stopPriorityRefresh(); + try { + IMonitorDataGeneral generalData = monitorProxy.getMonitorData(); + logger.debug("{}: context='onFetchData' tag='monitorData' URL='{}' ResponseCode='{}' ResponseMessage='{}'", + getLogIdentifier(), generalData.getHttpRequestUrl(), generalData.getHttpStatus(), + generalData.getHttpResponseMessage()); + + dataConverter.setMonitorGeneralData(generalData); + } catch (ZoneMinderInvalidData zmid) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getMonitorData' Exception='{}' Response='{}', Message='{}'", + getLogIdentifier(), zmid.getClass().getCanonicalName(), zmid.getResponseString(), zmid.getMessage(), + zmid.getCause()); + } catch (ZoneMinderAuthenticationException | ZoneMinderGeneralException | ZoneMinderResponseException zme) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getMonitorData' Exception='{}' Message='{}'", + getLogIdentifier(), zme.getClass().getCanonicalName(), zme.getMessage(), zme.getCause()); } + try { + ZoneMinderMonitorStatus status = monitorProxy.getMonitorDetailedStatus(); + + logger.debug( + "{}: context='onFetchData' tag='detailedStatus' URL='{}' ResponseCode='{}' ResponseMessage='{}'", + getLogIdentifier(), status.getHttpRequestUrl(), status.getHttpStatus(), + status.getHttpResponseMessage()); + + dataConverter.setMonitorDetailedStatus(status.getStatus()); + } catch (ZoneMinderInvalidData zmid) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getMonitorDetailedStatus' Exception='{}', Message='{}', Response='{}'", + getLogIdentifier(), zmid.getClass().getCanonicalName(), zmid.getMessage(), zmid.getResponseString(), + zmid.getCause()); + } catch (ZoneMinderAuthenticationException | ZoneMinderGeneralException | ZoneMinderResponseException zme) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getMonitorDetailedStatus' Exception='{}' Message='{}'", + getLogIdentifier(), zme.getClass().getCanonicalName(), zme.getMessage(), zme.getCause()); + } finally { + if (doRelase) { + releaseSession(); + } + } + lastRefreshGeneralData = System.currentTimeMillis(); } - protected State getDetailedStatus() { - State state = UnDefType.UNDEF; + void fetchMonitorDaemonStatus(boolean fetchCapture, boolean fetchAnalysisFrame, IZoneMinderMonitor proxy) { + IZoneMinderMonitor monitorProxy = proxy; + boolean fetchFrame = false; + boolean doRelase = false; + if (monitorProxy == null) { + doRelase = true; + monitorProxy = ZoneMinderFactory.getMonitorProxy(aquireSession(), getZoneMinderId()); + } try { - if (channelMonitorStatus == ZoneMinderMonitorStatusEnum.UNKNOWN) { - state = getChannelStringAsStringState(""); - } else { - state = getChannelStringAsStringState(channelMonitorStatus.toString()); + State stateCapture = UnDefType.UNDEF; + State stateAnalysis = UnDefType.UNDEF; + State stateFrame = UnDefType.UNDEF; + + IZoneMinderDaemonStatus captureDaemon = null; + IZoneMinderDaemonStatus analysisDaemon = null; + IZoneMinderDaemonStatus frameDaemon = null; + + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_CAPTURE_DAEMON_STATE)) { + try { + if (fetchCapture) { + captureDaemon = monitorProxy.getCaptureDaemonStatus(); + logger.debug( + "{}: context='fetchMonitorDaemonStatus' tag='captureDaemon' URL='{}' ResponseCode='{}' ResponseMessage='{}'", + getLogIdentifier(), captureDaemon.getHttpRequestUrl(), captureDaemon.getHttpStatus(), + captureDaemon.getHttpResponseMessage()); + stateCapture = (captureDaemon.getStatus() ? OnOffType.ON : OnOffType.OFF); + } + } catch (ZoneMinderResponseException zmre) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getCaptureDaemonStatus' - Http: Status='{}', Message='{}', ExceptionMessage='{}', Exception='{}', Message={}'", + getLogIdentifier(), zmre.getHttpStatus(), zmre.getHttpMessage(), zmre.getExceptionMessage(), + zmre.getClass().getCanonicalName(), zmre.getMessage(), zmre.getCause()); + } catch (ZoneMinderInvalidData zmid) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getCaptureDaemonStatus' - Response='{}', Exception='{}', Message={}'", + getLogIdentifier(), zmid.getResponseString(), zmid.getClass().getCanonicalName(), + zmid.getMessage(), zmid.getCause()); + + } catch (ZoneMinderGeneralException | ZoneMinderAuthenticationException zme) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getCaptureDaemonStatus' - Exception='{}', Message={}' ", + getLogIdentifier(), zme.getClass().getCanonicalName(), zme.getMessage(), zme.getCause()); + } finally { + if (captureDaemon != null) { + dataConverter.setMonitorCaptureDaemonStatus(stateCapture); + } + + } } - } catch (Exception ex) { - logger.debug("{}", ex.getMessage()); - } + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_ANALYSIS_DAEMON_STATE)) { + try { + stateAnalysis = UnDefType.UNDEF; + if (fetchAnalysisFrame) { + analysisDaemon = monitorProxy.getAnalysisDaemonStatus(); + logger.debug( + "{}: context='onFetchData' tag='analysisDaemon' URL='{}' ResponseCode='{}' ResponseMessage='{}'", + getLogIdentifier(), analysisDaemon.getHttpRequestUrl(), analysisDaemon.getHttpStatus(), + analysisDaemon.getHttpResponseMessage()); + + stateAnalysis = (analysisDaemon.getStatus() ? OnOffType.ON : OnOffType.OFF); + fetchFrame = true; + } - return state; + } catch (ZoneMinderResponseException zmre) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getAnalysisDaemonStatus' - Http: Status='{}', Message='{}', ExceptionMessage='{}', Exception='{}'", + getLogIdentifier(), zmre.getHttpStatus(), zmre.getHttpMessage(), zmre.getExceptionMessage(), + zmre.getClass().getCanonicalName(), zmre.getCause()); + } catch (ZoneMinderInvalidData zmid) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getAnalysisDaemonStatus' - Response='{}', Exception='{}'", + getLogIdentifier(), zmid.getResponseString(), zmid.getClass().getCanonicalName(), + zmid.getCause()); + + } catch (ZoneMinderGeneralException | ZoneMinderAuthenticationException zme) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getAnalysisDaemonStatus' - Exception='{}' ", + getLogIdentifier(), zme.getClass().getCanonicalName(), zme.getCause()); + } catch (Exception ex) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' tag='exception' error in call to 'getAnalysisDaemonStatus' - Exception='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex); + } finally { + dataConverter.setMonitorAnalysisDaemonStatus(stateAnalysis); + } + } + + if (isLinked(ZoneMinderConstants.CHANNEL_MONITOR_FRAME_DAEMON_STATE)) { + try { + stateFrame = UnDefType.UNDEF; + if ((fetchFrame) && frameDaemonActive) { + frameDaemon = monitorProxy.getFrameDaemonStatus(); + logger.debug( + "{}: context='fetchMonitorDaemonStatus' tag='frameDaemon' URL='{}' ResponseCode='{}' ResponseMessage='{}'", + getLogIdentifier(), frameDaemon.getHttpRequestUrl(), frameDaemon.getHttpStatus(), + frameDaemon.getHttpResponseMessage()); + + if (frameDaemon != null) { + stateFrame = ((frameDaemon.getStatus() && analysisDaemon.getStatus()) ? OnOffType.ON + : OnOffType.OFF); + } + } + } catch (ZoneMinderResponseException zmre) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getFrameDaemonStatus' - Http: Status='{}', Message='{}', ExceptionMessage'{}', Exception='{}'", + getLogIdentifier(), zmre.getHttpStatus(), zmre.getHttpMessage(), zmre.getExceptionMessage(), + zmre.getClass().getCanonicalName(), zmre.getCause()); + } catch (ZoneMinderInvalidData zmid) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getFrameDaemonStatus' - Response='{}', Exception='{}'", + getLogIdentifier(), zmid.getResponseString(), zmid.getClass().getCanonicalName(), + zmid.getCause()); + + } catch (ZoneMinderGeneralException | ZoneMinderAuthenticationException zme) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' error in call to 'getFrameDaemonStatus' - Exception='{}'", + getLogIdentifier(), zme.getClass().getCanonicalName(), zme.getCause()); + } catch (Exception ex) { + logger.error( + "{}: context='fetchMonitorDaemonStatus' tag='exception' error in call to 'getFrameDaemonStatus' - Exception='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex); + } finally { + dataConverter.setMonitorFrameDaemonStatus(stateFrame); + } + } + + } finally { + if (doRelase) { + releaseSession(); + } + } } /* * This is experimental * Try to add different properties */ - private void updateMonitorProperties(IZoneMinderSession session) { + private void updateMonitorProperties() { logger.debug("{}: Update Monitor Properties", getLogIdentifier()); // Update property information about this device Map properties = editProperties(); - IZoneMinderMonitor monitorProxy = ZoneMinderFactory.getMonitorProxy(session, getZoneMinderId()); - IZoneMinderMonitorData monitorData = monitorProxy.getMonitorData(); - logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), - monitorProxy.getHttpUrl(), monitorProxy.getHttpResponseCode(), monitorProxy.getHttpResponseMessage()); + IZoneMinderMonitor monitorProxy = null; + IMonitorDataGeneral monitorData = null; + IZoneMinderConnectionHandler session = null; + try { + session = aquireSession(); + + if (session == null) { + logger.error("{}: context='updateMonitorProperties' Unable to aquire session.", getLogIdentifier()); + return; + } + monitorProxy = ZoneMinderFactory.getMonitorProxy(session, getZoneMinderId()); + monitorData = monitorProxy.getMonitorData(); + logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(), + monitorData.getHttpRequestUrl(), monitorData.getHttpStatus(), monitorData.getHttpResponseMessage()); - properties.put(ZoneMinderProperties.PROPERTY_ID, getLogIdentifier()); - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_NAME, monitorData.getName()); + } catch (Exception e) { + logger.error("{}: Exception occurred when updating monitor properties - Message:{}", getLogIdentifier(), + e.getMessage()); - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_SOURCETYPE, monitorData.getSourceType().name()); + } catch (ZoneMinderException ex) { + logger.error( + "{}: context='onFetchData' error in call to 'getMonitorData' ExceptionClass='{}' - Message='{}'", + getLogIdentifier(), ex.getClass().getCanonicalName(), ex.getMessage(), ex.getCause()); + } finally { + if (session != null) { + releaseSession(); + } + } - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_ANALYSIS_FPS, monitorData.getAnalysisFPS()); - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_MAXIMUM_FPS, monitorData.getMaxFPS()); - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_ALARM_MAXIMUM, monitorData.getAlarmMaxFPS()); + if (monitorData != null) { + properties.put(ZoneMinderProperties.PROPERTY_ID, getLogIdentifier()); + properties.put(ZoneMinderProperties.PROPERTY_NAME, monitorData.getName()); - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_IMAGE_WIDTH, monitorData.getWidth()); - properties.put(ZoneMinderProperties.PROPERTY_MONITOR_IMAGE_HEIGHT, monitorData.getHeight()); + properties.put(ZoneMinderProperties.PROPERTY_MONITOR_SOURCETYPE, monitorData.getSourceType().name()); + properties.put(ZoneMinderProperties.PROPERTY_MONITOR_ANALYSIS_FPS, monitorData.getAnalysisFPS()); + properties.put(ZoneMinderProperties.PROPERTY_MONITOR_MAXIMUM_FPS, monitorData.getMaxFPS()); + properties.put(ZoneMinderProperties.PROPERTY_MONITOR_ALARM_MAXIMUM, monitorData.getAlarmMaxFPS()); + + properties.put(ZoneMinderProperties.PROPERTY_MONITOR_IMAGE_WIDTH, monitorData.getWidth()); + properties.put(ZoneMinderProperties.PROPERTY_MONITOR_IMAGE_HEIGHT, monitorData.getHeight()); + } // Must loop over the new properties since we might have added data boolean update = false; Map originalProperties = editProperties(); for (String property : properties.keySet()) { if ((originalProperties.get(property) == null - || originalProperties.get(property).equals(properties.get(property)) == false)) { + || !originalProperties.get(property).equals(properties.get(property)))) { update = true; break; } } - if (update == true) { - logger.debug("{}: Properties synchronised", getLogIdentifier()); + if (update) { + logger.debug("{}: context='updateMonitorProperties' Properties synchronised", getLogIdentifier()); updateProperties(properties); } } @@ -893,14 +1194,28 @@ public String getLogIdentifier() { try { if (config != null) { - result = String.format("[MONITOR-%s]", config.getZoneMinderId().toString()); } } catch (Exception ex) { result = "[MONITOR]"; } - return result; } + + @Override + public void onStateChanged(ChannelUID channelUID, State state) { + logger.debug("{}: context='onStateChanged' channel='{}' - State changed to '{}'", getLogIdentifier(), + channelUID.getAsString(), state.toString()); + updateState(channelUID.getId(), state); + } + + @Override + public void onRefreshDisabled() { + } + + @Override + public void onRefreshEnabled() { + } + } diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingType.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingType.java index b8771a14a021f..a3c950154efdc 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingType.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/handler/ZoneMinderThingType.java @@ -11,7 +11,7 @@ /** * Enumerator for each Bridge and Thing * - * @author Martin S. Eskildsen + * @author Martin S. Eskildsen - Initial contribution */ public enum ZoneMinderThingType { ZoneMinderServerBridge, diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/DataRefreshPriorityEnum.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/DataRefreshPriorityEnum.java deleted file mode 100644 index ff968f1bfc42d..0000000000000 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/DataRefreshPriorityEnum.java +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2010-2018 by the respective copyright holders. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.zoneminder.internal; - -/** - * - * @author Martin S. Eskildsen - Initial contribution - */ -public enum DataRefreshPriorityEnum { - SCHEDULED, - HIGH_PRIORITY; -} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/RefreshPriority.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/RefreshPriority.java new file mode 100644 index 0000000000000..abace17b0fd5f --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/RefreshPriority.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal; + +import java.util.HashMap; +import java.util.Map; + +import org.openhab.binding.zoneminder.ZoneMinderConstants; + +/** + * + * @author Martin S. Eskildsen - Initial contribution + */ +public enum RefreshPriority { + PRIORITY_BATCH(1), + PRIORITY_LOW(2), + PRIORITY_NORMAL(3), + PRIORITY_HIGH(4), + PRIORITY_ALARM(10), + DISABLED(0), + UNKNOWN(-1); + + private int value; + private static Map map = new HashMap<>(); + + private RefreshPriority(int value) { + this.value = value; + } + + public static RefreshPriority valueOf(int pageType) { + return (RefreshPriority) map.get(pageType); + } + + public int getValue() { + return value; + } + + public static RefreshPriority fromConfigValue(String value) { + if (value.equalsIgnoreCase(ZoneMinderConstants.CONFIG_VALUE_REFRESH_DISABLED)) { + return DISABLED; + } else if (value.equalsIgnoreCase(ZoneMinderConstants.CONFIG_VALUE_REFRESH_BATCH)) { + return RefreshPriority.PRIORITY_BATCH; + } else if (value.equalsIgnoreCase(ZoneMinderConstants.CONFIG_VALUE_REFRESH_LOW)) { + return RefreshPriority.PRIORITY_LOW; + } else if (value.equalsIgnoreCase(ZoneMinderConstants.CONFIG_VALUE_REFRESH_NORMAL)) { + return RefreshPriority.PRIORITY_NORMAL; + } else if (value.equalsIgnoreCase(ZoneMinderConstants.CONFIG_VALUE_REFRESH_HIGH)) { + return RefreshPriority.PRIORITY_HIGH; + } else if (value.equalsIgnoreCase(ZoneMinderConstants.CONFIG_VALUE_REFRESH_ALARM)) { + return RefreshPriority.PRIORITY_ALARM; + } + return UNKNOWN; + + } + + public boolean isEqual(RefreshPriority refrenceVal) { + if (value == refrenceVal.getValue()) { + return true; + } + return false; + } + + public boolean isLessThan(RefreshPriority refrenceVal) { + if (value < refrenceVal.getValue()) { + return true; + } + return false; + } + + public boolean isGreaterThan(RefreshPriority refrenceVal) { + if (value > refrenceVal.getValue()) { + return true; + } + return false; + } + + public boolean isPriorityActive(RefreshPriority refrenceVal) { + if (value <= refrenceVal.getValue()) { + return true; + } + return false; + } +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/ZoneMinderConnectionStatus.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/ZoneMinderConnectionStatus.java new file mode 100644 index 0000000000000..74d3128409101 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/ZoneMinderConnectionStatus.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.openhab.binding.zoneminder.internal; + +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link ZoneMinderConnectionStatus} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public enum ZoneMinderConnectionStatus { + GENERAL_ERROR(-99), + BINDING_CONFIG_INVALID(-98), + BINDING_CONNECTION_INVALID(-97), + BINDING_SESSION_INVALID(-96), + + SERVER_CREDENTIALS_INVALID(-89), + SERVER_API_DISABLED(-88), + SERVER_OPT_TRIGGERS_DISABLED(-86), + ERROR_RECOVERABLE(-20), + SERVER_DAEMON_NOT_RUNNING(-19), + BINDING_CONNECTION_TIMEOUT(-1), + UNINITIALIZED(0), + BINDING_CONFIG_LOAD_PASSED(1), + BINDING_CONFIG_VALIDATE_PASSED(2), + ZONEMINDER_CONNECTION_CREATED(3), + ZONEMINDER_API_ACCESS_PASSED(4), + ZONEMINDER_SESSION_CREATED(5), + ZONEMINDER_SERVER_CONFIG_PASSED(6), + INITIALIZED(10); + + private int value; + private static Map map = new HashMap<>(); + + private ZoneMinderConnectionStatus(int value) { + this.value = value; + } + + public static ZoneMinderConnectionStatus valueOf(int pageType) { + return (ZoneMinderConnectionStatus) map.get(pageType); + } + + public boolean lessThan(ZoneMinderConnectionStatus reference) { + return (getValue() < reference.getValue()) ? true : false; + } + + public boolean greatherThan(ZoneMinderConnectionStatus reference) { + return (getValue() > reference.getValue()) ? true : false; + } + + public boolean greatherThanEqual(ZoneMinderConnectionStatus reference) { + return (getValue() >= reference.getValue()) ? true : false; + } + + public boolean isErrorState() { + return lessThan(UNINITIALIZED); + } + + public boolean hasUnrecoverableError() { + return lessThan(ERROR_RECOVERABLE); + } + + public boolean hasPassed(ZoneMinderConnectionStatus reference) { + return greatherThanEqual(reference); + } + + public int getValue() { + return value; + } + + public boolean hasRecoverableError() { + return isErrorState() && !hasUnrecoverableError(); + } +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderBridgeServerConfig.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderBridgeServerConfig.java index c7190eaa07fac..c8a7287eef0d3 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderBridgeServerConfig.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderBridgeServerConfig.java @@ -9,63 +9,70 @@ package org.openhab.binding.zoneminder.internal.config; import org.openhab.binding.zoneminder.ZoneMinderConstants; +import org.openhab.binding.zoneminder.internal.RefreshPriority; /** * Configuration data according to zoneminderserver.xml * - * @author Martin S. Eskildsen + * @author Martin S. Eskildsen - Initial contribution * */ public class ZoneMinderBridgeServerConfig extends ZoneMinderConfig { - private String hostname; - private Integer http_port; - private Integer telnet_port; + private Integer portHttp; + private Integer portTelnet; private String protocol; + private String host; - private String urlpath; + private String urlSite; + private String urlApi; private String user; private String password; - private Integer refresh_interval; - private Integer refresh_interval_disk_usage; - private Boolean autodiscover_things; + private Integer refreshNormal; + private Integer refreshLow; + private String diskUsageRefresh; + private Boolean autodiscover; + + private Boolean useSpecificUserStreaming; + private String streamingUser; + private String streamingPassword; @Override public String getConfigId() { return ZoneMinderConstants.BRIDGE_ZONEMINDER_SERVER; } - public String getHostName() { - return hostname; + public String getHost() { + return host; } - public void setHostName(String hostName) { - this.hostname = hostName; + public void setHostName(String host) { + this.host = host; } public Integer getHttpPort() { - if ((http_port == null) || (http_port == 0)) { + if ((portHttp == null) || (portHttp == 0)) { if (getProtocol().equalsIgnoreCase("http")) { - http_port = 80; + portHttp = 80; } else { - http_port = 443; + portHttp = 443; } } - return http_port; + return portHttp; } public void setHttpPort(Integer port) { - this.http_port = port; + this.portHttp = port; } public Integer getTelnetPort() { - return telnet_port; + return portTelnet; } public void setTelnetPort(Integer telnetPort) { - this.telnet_port = telnetPort; + this.portTelnet = telnetPort; } public String getProtocol() { @@ -77,11 +84,19 @@ public void setProtocol(String protocol) { } public String getServerBasePath() { - return urlpath; + return urlSite; } public void setServerBasePath(String urlpath) { - this.urlpath = urlpath; + this.urlSite = urlpath; + } + + public String getServerApiPath() { + return urlApi; + } + + public void setServerApiPath(String apiPath) { + this.urlApi = apiPath; } public String getUserName() { @@ -100,28 +115,49 @@ public void setPassword(String password) { this.password = password; } - public Integer getRefreshInterval() { - return refresh_interval; + public Integer getRefreshIntervalNormal() { + return this.refreshNormal; } - public void setRefreshInterval(Integer refreshInterval) { - this.refresh_interval = refreshInterval; + public void setRefreshIntervalNormal(Integer refreshInterval) { + this.refreshNormal = refreshInterval; } - public Integer getRefreshIntervalLowPriorityTask() { - return refresh_interval_disk_usage; + public Integer getRefreshIntervalLow() { + return this.refreshLow; } - public void setRefreshIntervalDiskUsage(Integer refreshIntervalDiskUsage) { - this.refresh_interval_disk_usage = refreshIntervalDiskUsage; + public void setRefreshIntervalDiskUsage(Integer refreshInterval) { + this.refreshLow = refreshInterval; } public Boolean getAutodiscoverThings() { - return autodiscover_things; + return autodiscover; } public void setAutodiscoverThings(Boolean autodiscoverThings) { - this.autodiscover_things = autodiscoverThings; + this.autodiscover = autodiscoverThings; + } + + public RefreshPriority getDiskUsageRefresh() { + return getRefreshPriorityEnum(diskUsageRefresh); } + public Boolean getUseSpecificUserStreaming() { + return useSpecificUserStreaming; + } + + public String getStreamingUser() { + if (!getUseSpecificUserStreaming()) { + return getUserName(); + } + return streamingUser; + } + + public String getStreamingPassword() { + if (!getUseSpecificUserStreaming()) { + return getPassword(); + } + return streamingPassword; + } } diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderConfig.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderConfig.java index 0ca0820dd3ec4..50e3cf3cd3d89 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderConfig.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderConfig.java @@ -8,11 +8,21 @@ */ package org.openhab.binding.zoneminder.internal.config; +import org.openhab.binding.zoneminder.internal.RefreshPriority; + /** * base class containing Configuration in openHAB * - * @author Martin S. Eskildsen + * @author Martin S. Eskildsen - Initial contribution */ public abstract class ZoneMinderConfig { public abstract String getConfigId(); + + protected RefreshPriority getRefreshPriorityEnum(String configValue) { + RefreshPriority priority = RefreshPriority.fromConfigValue(configValue); + if (priority != RefreshPriority.UNKNOWN) { + return priority; + } + return RefreshPriority.UNKNOWN; + } } diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderThingMonitorConfig.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderThingMonitorConfig.java index e702fa64f9aa1..b08b0307dbe20 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderThingMonitorConfig.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/config/ZoneMinderThingMonitorConfig.java @@ -9,6 +9,7 @@ package org.openhab.binding.zoneminder.internal.config; import org.openhab.binding.zoneminder.ZoneMinderConstants; +import org.openhab.binding.zoneminder.internal.RefreshPriority; /** * Specific configuration class for Monitor COnfig. @@ -18,7 +19,19 @@ public class ZoneMinderThingMonitorConfig extends ZoneMinderThingConfig { // Parameters - private Integer monitorId; + private Integer id; + private Integer triggerTimeout; + private String eventText; + private String imageRefreshIdle; + private String imageRefreshEvent; + // private String daemonRefresh; + private Integer imageScale; + + // private Integer max_image_size; + + // private Boolean enable_image_updates; + // private String video_encoding; + // private Integer video_framerate; @Override public String getConfigId() { @@ -26,11 +39,64 @@ public String getConfigId() { } public String getId() { - return monitorId.toString(); + return id.toString(); } @Override public String getZoneMinderId() { - return monitorId.toString(); + return id.toString(); } -} + + public RefreshPriority getImageRefreshIdle() { + return getRefreshPriorityEnum(imageRefreshIdle); + } + + public RefreshPriority getImageRefreshEvent() { + return getRefreshPriorityEnum(imageRefreshEvent); + } + + /* + * public RefreshPriority getDaemonRefresh() { + * return getRefreshPriorityEnum(daemonRefresh); + * } + */ + public Integer getImageScale() { + return imageScale; + } + + /* + * public Integer getMaxImageSize() { + * return max_image_size; + * } + * + * public void setMaxImageSize(Integer size) { + * this.max_image_size = size; + * } + */ + /* + * public Boolean getEnableImageUpdates() { + * return enable_image_updates; + * } + * + * public void setEnableImageUpdates(Boolean enable_image_updates) { + * this.enable_image_updates = enable_image_updates; + * } + */ + /* + * public String getVideoEncoding() { + * return video_encoding; + * } + * + * public void setVideoEncoding(String video_encoding) { + * this.video_encoding = video_encoding; + * } + * + * public Integer getVideoFramerate() { + * return video_framerate; + * } + * + * public void setVideoFramerate(Integer video_framerate) { + * this.video_framerate = video_framerate; + * } + * + */} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/discovery/ZoneMinderDiscoveryService.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/discovery/ZoneMinderDiscoveryService.java index bf62a4fb14d6a..cb22a90b0f2ac 100644 --- a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/discovery/ZoneMinderDiscoveryService.java +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/discovery/ZoneMinderDiscoveryService.java @@ -25,7 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import name.eskildsen.zoneminder.IZoneMinderMonitorData; +import name.eskildsen.zoneminder.data.IMonitorDataGeneral; /** * @@ -65,12 +65,15 @@ public Set getSupportedThingTypes() { @Override public void startBackgroundDiscovery() { logger.debug("[DISCOVERY]: Performing background discovery scan for {}", serverHandler.getThing().getUID()); + + // removeOlderResults(getTimestampOfLastScan()); discoverMonitors(); } @Override public void startScan() { logger.debug("[DISCOVERY]: Starting discovery scan for {}", serverHandler.getThing().getUID()); + discoverMonitors(); } @@ -84,55 +87,67 @@ protected synchronized void stopScan() { super.stopScan(); } - protected String BuildMonitorLabel(String id, String name) { + private String buildMonitorLabel(String id, String name) { return String.format("%s [%s]", ZoneMinderConstants.ZONEMINDER_MONITOR_NAME, name); } protected synchronized void discoverMonitors() { - // Add all existing devices - for (IZoneMinderMonitorData monitor : serverHandler.getMonitors()) { - deviceAdded(monitor); + for (IMonitorDataGeneral monitorData : serverHandler.getMonitors()) { + DiscoveryResult curDiscoveryResult = null; + ThingUID thingUID = getMonitorThingUID(monitorData); + + // Avoid issue #5143 in Eclipse SmartHome + DiscoveryResult existingResult = discoveryServiceCallback.getExistingDiscoveryResult(thingUID); + if ((existingResult != null) && (existingResult.getThingUID() != thingUID)) { + existingResult = null; + } + + if (existingResult != null) { + logger.debug("[DISCOVERY]: Monitor with Id='{}' and Name='{}' with ThingUID='{}' already discovered", + monitorData.getId(), monitorData.getName(), thingUID); + + } else if (discoveryServiceCallback.getExistingThing(thingUID) != null) { + logger.debug("[DISCOVERY]: Monitor with Id='{}' and Name='{}' with ThingUID='{}' already added", + monitorData.getId(), monitorData.getName(), thingUID); + } else { + curDiscoveryResult = createMonitorDiscoveryResult(thingUID, monitorData); + + } + + if (curDiscoveryResult != null) { + logger.info("[DISCOVERY]: Monitor with Id='{}' and Name='{}' added to Inbox with ThingUID='{}'", + monitorData.getId(), monitorData.getName(), thingUID); + thingDiscovered(curDiscoveryResult); + } } } - private boolean monitorThingExists(ThingUID newThingUID) { - return serverHandler.getThingByUID(newThingUID) != null ? true : false; - } + private ThingUID getMonitorThingUID(IMonitorDataGeneral monitor) { + ThingUID bridgeUID = serverHandler.getThing().getUID(); + String monitorUID = String.format("%s-%s", ZoneMinderConstants.THING_ZONEMINDER_MONITOR, monitor.getId()); + + return new ThingUID(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR, bridgeUID, monitorUID); - /** - * This is called once the node is fully discovered. At this point we know most of the information about - * the device including manufacturer information. - * - * @param node the node to be added - */ + } - public void deviceAdded(IZoneMinderMonitorData monitor) { + protected DiscoveryResult createMonitorDiscoveryResult(ThingUID monitorUID, IMonitorDataGeneral monitorData) { try { ThingUID bridgeUID = serverHandler.getThing().getUID(); - String monitorUID = String.format("%s-%s", ZoneMinderConstants.THING_ZONEMINDER_MONITOR, monitor.getId()); - ThingUID thingUID = new ThingUID(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR, bridgeUID, - monitorUID); - - // Does Monitor exist? - if (!monitorThingExists(thingUID)) { - logger.info("[DISCOVERY]: Monitor with Id='{}' and Name='{}' added", monitor.getId(), - monitor.getName()); - Map properties = new HashMap<>(0); - properties.put(ZoneMinderConstants.PARAMETER_MONITOR_ID, Integer.valueOf(monitor.getId())); - properties.put(ZoneMinderConstants.PARAMETER_MONITOR_TRIGGER_TIMEOUT, - ZoneMinderConstants.PARAMETER_MONITOR_TRIGGER_TIMEOUT_DEFAULTVALUE); - properties.put(ZoneMinderConstants.PARAMETER_MONITOR_EVENTTEXT, - ZoneMinderConstants.MONITOR_EVENT_OPENHAB); - - DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties) - .withBridge(bridgeUID).withLabel(BuildMonitorLabel(monitor.getId(), monitor.getName())).build(); - - thingDiscovered(discoveryResult); - } + + Map properties = new HashMap<>(0); + properties.put(ZoneMinderConstants.PARAMETER_MONITOR_ID, Integer.valueOf(monitorData.getId())); + properties.put(ZoneMinderConstants.PARAMETER_MONITOR_TRIGGER_TIMEOUT, + ZoneMinderConstants.PARAMETER_MONITOR_TRIGGER_TIMEOUT_DEFAULTVALUE); + properties.put(ZoneMinderConstants.PARAMETER_MONITOR_EVENTTEXT, ZoneMinderConstants.MONITOR_EVENT_OPENHAB); + + return DiscoveryResultBuilder.create(monitorUID).withProperties(properties).withBridge(bridgeUID) + .withLabel(buildMonitorLabel(monitorData.getId(), monitorData.getName())).build(); + } catch (Exception ex) { - logger.error("[DISCOVERY]: Error occurred when calling 'monitorAdded' from Discovery. Exception={}", - ex.getMessage()); + logger.error( + "[DISCOVERY]: Error occurred when calling 'monitorAdded' from Discovery. Id='{}', Name='{}', ThingUID='{}'", + monitorData.getId(), monitorData.getName(), monitorUID, ex.getCause()); } - + return null; } } diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelOnOffType.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelOnOffType.java new file mode 100644 index 0000000000000..ceb3f4bf7b599 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelOnOffType.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.openhab.binding.zoneminder.internal.state; + +import javax.activation.UnsupportedDataTypeException; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; + +/** + * The {@link ChannelOnOffType} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ + +public class ChannelOnOffType extends GenericChannelState { + + protected ChannelOnOffType(ChannelUID channelUID, GenericThingState thing, + ChannelStateChangeSubscriber subscriber) { + super(channelUID, thing, subscriber); + } + + @Override + protected State convert(Object _state) throws UnsupportedDataTypeException { + State newState = UnDefType.UNDEF; + if (_state instanceof String) { + String value = (String) _state; + if (((String) _state).equalsIgnoreCase("ON")) { + newState = OnOffType.ON; + } else if (((String) _state).equalsIgnoreCase("OFF")) { + newState = OnOffType.OFF; + } else { + throw new UnsupportedDataTypeException(); + } + } else if (_state instanceof Boolean) { + newState = ((Boolean) _state) ? OnOffType.ON : OnOffType.OFF; + } else if (_state instanceof OnOffType) { + newState = (OnOffType) _state; + } else if (_state instanceof UnDefType) { + newState = (UnDefType) _state; + } else { + throw new UnsupportedDataTypeException(); + } + return newState; + } + +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelRawType.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelRawType.java new file mode 100644 index 0000000000000..d776e6359b9a1 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelRawType.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal.state; + +import java.io.ByteArrayOutputStream; + +import javax.activation.UnsupportedDataTypeException; + +import org.eclipse.smarthome.core.library.types.RawType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; + +/** + * The {@link ChannelRawType} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public class ChannelRawType extends GenericChannelState { + + protected ChannelRawType(ChannelUID channelUID, GenericThingState thing, ChannelStateChangeSubscriber subscriber) { + super(channelUID, thing, subscriber); + } + + @Override + protected State convert(Object _state) throws UnsupportedDataTypeException { + State newState = UnDefType.UNDEF; + + State state = UnDefType.UNDEF; + if (_state instanceof ByteArrayOutputStream) { + // ByteArrayOutputStream baos = (ByteArrayOutputStream) _state; + newState = new RawType(((ByteArrayOutputStream) _state).toByteArray(), "image/jpeg"); + } + + else if (_state instanceof RawType) { + newState = (RawType) _state; + } else if (_state instanceof UnDefType) { + newState = (UnDefType) _state; + } else { + throw new UnsupportedDataTypeException(); + } + return newState; + + } + +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelState.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelState.java new file mode 100644 index 0000000000000..d1a25e9c8a7fa --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelState.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.openhab.binding.zoneminder.internal.state; + +import javax.activation.UnsupportedDataTypeException; + +import org.eclipse.smarthome.core.types.State; + +/** + * The {@link ChannelState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +interface ChannelState { + + public void subscribe(); + + public void unsubscribe(); + + public State getState(); + + // private State statePublished = UnDefType.NULL; + // private Type dataType = UnDefType.class; + + public void setState(Object state) throws UnsupportedDataTypeException; + + public void setState(Object state, boolean update) throws UnsupportedDataTypeException; + +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangePublisher.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangePublisher.java new file mode 100644 index 0000000000000..a1e2778768ac9 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangePublisher.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal.state; + +import org.eclipse.smarthome.core.thing.ChannelUID; + +/** + * The {@link GenericThingState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public interface ChannelStateChangePublisher { + /* + * public void subscribeStringType(ChannelUID channelUID, ChannelStateChangeSubscriber subscriber); + * + * public void subscribeOnOffType(ChannelUID channelUID, ChannelStateChangeSubscriber subscriber); + * + * public void subscribeNumberType(ChannelUID channelUID, ChannelStateChangeSubscriber subscriber); + */ + // public void subscribeRawType(ChannelUID channelUID, ChannelStateChangeSubscriber subscriber); + + public GenericChannelState createChannelSubscription( + ChannelUID channelUID /* + * , + * ChannelStateChangeSubscriber subscriber + */); + + public void addChannel(ChannelUID channelUID/* , ChannelStateChangeSubscriber subscriber */); + + public void subscribe(ChannelUID channelUID/* , ChannelStateChangeSubscriber subscriber */); + + public void unsubscribe(ChannelUID channelUID); + + public void disableRefresh(); + + public void enableRefresh(); + + public boolean allowRefresh(); +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangeSubscriber.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangeSubscriber.java new file mode 100644 index 0000000000000..a88fb59dd3d86 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStateChangeSubscriber.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal.state; + +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; + +/** + * The {@link GenericThingState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public interface ChannelStateChangeSubscriber { + void onStateChanged(ChannelUID channelUID, State state); + + void onRefreshDisabled(); + + void onRefreshEnabled(); +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStringType.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStringType.java new file mode 100644 index 0000000000000..e96e512344bb6 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/ChannelStringType.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal.state; + +import javax.activation.UnsupportedDataTypeException; + +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; + +/** + * The {@link GenericThingState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public class ChannelStringType extends GenericChannelState { + + protected ChannelStringType(ChannelUID channelUID, GenericThingState thing, + ChannelStateChangeSubscriber subscriber) { + super(channelUID, thing, subscriber); + } + + @Override + protected State convert(Object _state) throws UnsupportedDataTypeException { + State newState = UnDefType.UNDEF; + + if (_state instanceof String) { + newState = new StringType((String) _state); + } else if (_state instanceof StringType) { + newState = (StringType) _state; + + } else if (_state instanceof UnDefType) { + newState = (UnDefType) _state; + } else { + throw new UnsupportedDataTypeException(); + } + return newState; + } + +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericChannelState.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericChannelState.java new file mode 100644 index 0000000000000..a0fb1ed407fcd --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericChannelState.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal.state; + +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.activation.UnsupportedDataTypeException; + +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; + +/** + * The {@link GenericThingState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public abstract class GenericChannelState implements ChannelState { + + private AtomicBoolean isDirty = new AtomicBoolean(false); + + private ChannelUID channelUID; + private GenericThingState thing = null; + private ChannelStateChangeSubscriber subscriber = null; + private State state = UnDefType.NULL; + private int countSubscription = 0; + + protected GenericChannelState(ChannelUID channelUID, GenericThingState thing, + ChannelStateChangeSubscriber subscriber) { + this.channelUID = channelUID; + this.thing = thing; + this.subscriber = subscriber; + } + + @Override + public void subscribe() { + countSubscription++; + } + + @Override + public void unsubscribe() { + if (countSubscription > 0) { + countSubscription--; + } + + } + + @Override + public State getState() { + return state; + } + + @Override + public void setState(Object objState) throws UnsupportedDataTypeException { + setState(objState, true); + } + + @Override + public void setState(Object objState, boolean update) throws UnsupportedDataTypeException { + State newState = convert(objState); + boolean changed = false; + + if (!(state.toString().equals(newState.toString()))) { + changed = true; + state = newState; + // Set Dirty flag + setDirtyFlag(); + + thing.onChannelChanged(channelUID); + } + + if (update && changed) { + // Try to udpate + flushChanges(); + } + } + + public void flushChanges() { + flushChanges(false); + } + + public void flushChanges(boolean force) { + if (!isDirty.get() && !force) { + return; + } + + if (thing.allowRefresh() || force) { + subscriber.onStateChanged(channelUID, state); + // Clear dirtyflag + clearDirtyFlag(); + } + } + + public void setDirtyFlag() { + isDirty.lazySet(true); + } + + public void clearDirtyFlag() { + isDirty.set(false); + } + + protected abstract State convert(Object state) throws UnsupportedDataTypeException; + +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericThingState.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericThingState.java new file mode 100644 index 0000000000000..bcfd201499dda --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/GenericThingState.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.openhab.binding.zoneminder.internal.state; + +import java.io.ByteArrayOutputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.RawType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link GenericThingState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public abstract class GenericThingState implements ChannelStateChangePublisher { + + private Logger logger = LoggerFactory.getLogger(GenericThingState.class); + + final AtomicBoolean allowRefresh = new AtomicBoolean(true); + + private ChannelStateChangeSubscriber subscriber = null; + + // Keeps track of current channel refresh status of subscribed channels + private Map subscriptions = new ConcurrentHashMap(); + + public GenericThingState(ChannelStateChangeSubscriber subscriber) { + this.subscriber = subscriber; + } + + protected abstract void recalculate(); + + public void onChannelChanged(ChannelUID channelUID) { + } + + @Override + public void addChannel(ChannelUID channelUID) { + try { + if (!subscriptions.containsKey(channelUID.getId())) { + subscriptions.put(channelUID.getId(), createChannelSubscription(channelUID)); + } + } catch ( + + Exception ex) { + logger.error("{}: context='subscribe' - Exception occurred when subscribing to channel '{}'", "", + channelUID.getId()); + } + } + + protected GenericChannelState createSubscriptionStringType(ChannelUID channelUID) { + return new ChannelStringType(channelUID, this, subscriber); + + } + + protected GenericChannelState createSubscriptionOnOffType(ChannelUID channelUID) { + return new ChannelOnOffType(channelUID, this, subscriber); + } + + protected GenericChannelState getChannelStateHandler(String channelId) { + return subscriptions.get(channelId); + + } + + public GenericChannelState getChannelStateHandler(ChannelUID channelUID) { + return getChannelStateHandler(channelUID.getId()); + + } + + protected GenericChannelState createSubscriptionRawType(ChannelUID channelUID) { + return new ChannelRawType(channelUID, this, subscriber); + } + + @Override + public void subscribe(ChannelUID channelUID/* , ChannelStateChangeSubscriber subscriber */) { + try { + if (getChannelStateHandler(channelUID) == null) { + addChannel(channelUID/* , subscriber */); + } + getChannelStateHandler(channelUID).subscribe(); + + } catch ( + + Exception ex) { + logger.error("{}: context='subscribe' - Exception occurred when subscribing to channel '{}'", "", + channelUID.getId(), ex); + } + } + + @Override + public void unsubscribe(ChannelUID channelUID) { + try { + if (getChannelStateHandler(channelUID) != null) { + getChannelStateHandler(channelUID).subscribe(); + } + } catch (Exception ex) { + logger.error("{}: context='unsubscribe' - Exception occurred when subscribing to channel '{}'", "", + channelUID.getId()); + } + } + + @Override + public void disableRefresh() { + allowRefresh.compareAndSet(true, false); + } + + @Override + public void enableRefresh() { + if (allowRefresh.compareAndSet(false, true)) { + for (Map.Entry entry : subscriptions.entrySet()) { + String key = entry.getKey().toString(); + GenericChannelState channel = entry.getValue(); + channel.flushChanges(); + } + } + } + + @Override + public boolean allowRefresh() { + return allowRefresh.get(); + } + + public void forceUpdate(boolean _recalculate) { + if (_recalculate) { + recalculate(); + } + + for (Map.Entry entry : subscriptions.entrySet()) { + String key = entry.getKey().toString(); + GenericChannelState channel = entry.getValue(); + channel.flushChanges(true); + } + } + + /** + * + * OLD STUFF Needs cleaning + * + */ + + protected State getStringAsStringState(String value) { + State state = UnDefType.UNDEF; + + if (value != null) { + state = new StringType(value); + } + + return state; + + } + + protected State getBooleanAsOnOffState(boolean value) { + State state = UnDefType.UNDEF; + state = value ? OnOffType.ON : OnOffType.OFF; + return state; + } + + protected State getImageByteArrayAsRawType(ByteArrayOutputStream baos) { + State state = UnDefType.UNDEF; + if (baos != null) { + state = new RawType(baos.toByteArray(), "image/jpeg"); + } + + return state; + } + +} diff --git a/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/MonitorThingState.java b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/MonitorThingState.java new file mode 100644 index 0000000000000..9df154c5002b4 --- /dev/null +++ b/addons/binding/org.openhab.binding.zoneminder/src/main/java/org/openhab/binding/zoneminder/internal/state/MonitorThingState.java @@ -0,0 +1,537 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.zoneminder.internal.state; + +import java.io.ByteArrayOutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.activation.UnsupportedDataTypeException; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.RawType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.zoneminder.ZoneMinderConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import name.eskildsen.zoneminder.api.telnet.ZoneMinderTriggerEvent; +import name.eskildsen.zoneminder.common.ZoneMinderMonitorFunctionEnum; +import name.eskildsen.zoneminder.common.ZoneMinderMonitorStatusEnum; +import name.eskildsen.zoneminder.data.IMonitorDataGeneral; +import name.eskildsen.zoneminder.data.IZoneMinderEventData; + +/** + * The {@link MonitorThingState} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Martin S. Eskildsen - Initial contribution + */ +public class MonitorThingState extends GenericThingState { + + private Logger logger = LoggerFactory.getLogger(MonitorThingState.class); + + private boolean isDirty; + private String logIdentifier = ""; + + private IZoneMinderEventData activeEvent = null; + private ZoneMinderTriggerEvent curTriggerEvent = null; + + /* + * Used in recalculate + */ + AtomicBoolean recordingFunction = new AtomicBoolean(false); + AtomicBoolean recordingDetailedState = new AtomicBoolean(false); + AtomicBoolean alarmedFunction = new AtomicBoolean(false); + AtomicBoolean alarmedDetailedState = new AtomicBoolean(false); + + // Monitor properties + + // Video Url + private State channelVideoUrl; + + boolean bRecalculating = false; + + /* + * public MonitorThingState(String id, ChannelStateChangeSubscriber subscriber) { + * super(subscriber); + * logIdentifier = id; + * initialize(); + * } + */ + protected void initialize() { + isDirty = true; + } + + public MonitorThingState(ChannelStateChangeSubscriber subscriber) { + super(subscriber); + + } + + @Override + public GenericChannelState createChannelSubscription(ChannelUID channelUID) { + GenericChannelState channelState = null; + try { + switch (channelUID.getId()) { + case ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS: + case ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE: + case ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION: + channelState = createSubscriptionStringType(channelUID); + break; + + case ZoneMinderConstants.CHANNEL_MONITOR_STILL_IMAGE: + channelState = createSubscriptionRawType(channelUID); + break; + default: + channelState = createSubscriptionOnOffType(channelUID); + break; + } + + } catch (Exception ex) { + logger.error("{}: context='subscribe' - Exception occurred when subscribing to channel '{}'", "", + channelUID.getId()); + } + return channelState; + } + + public boolean isDirty() { + return isDirty; + } + + public boolean isAlarmed() { + State stateEnabled = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_STATE).getState(); + return ((alarmedDetailedState.get() || (stateEnabled == OnOffType.ON)) ? true : false); + + } + + private boolean isEnabled() { + State stateEnabled = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_ENABLED).getState(); + if (stateEnabled == OnOffType.ON) { + return true; + } + return false; + } + + public ZoneMinderMonitorFunctionEnum getMonitorFunction() { + State stateFunction = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION).getState(); + return ZoneMinderMonitorFunctionEnum.getEnum(stateFunction.toString()); + } + + public String getMonitorEventCause() { + State stateEventCause = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE).getState(); + return stateEventCause.toString(); + } + + public boolean getMonitorEventMotion() { + State stateEventMotion = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_MOTION_EVENT).getState(); + return false; + + } + + public ZoneMinderMonitorStatusEnum getMonitorDetailedStatus() { + State stateStatus = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS).getState(); + ZoneMinderMonitorStatusEnum result = ZoneMinderMonitorStatusEnum.UNKNOWN; + + if (stateStatus == UnDefType.NULL || stateStatus == UnDefType.UNDEF) { + return ZoneMinderMonitorStatusEnum.UNKNOWN; + } + + try { + result = ZoneMinderMonitorStatusEnum.getEnumFromName(stateStatus.toString()); + } catch (Exception ex) { + logger.error( + "{}: context='getMonitorDetailedStatus' Exception occurred when calling getMonitorDetailedStatus", + logIdentifier, ex); + + } + return result; + } + + public void setMonitorTriggerEvent(ZoneMinderTriggerEvent event) { + isDirty = true; + curTriggerEvent = event; + } + + public void setMonitorFunction(ZoneMinderMonitorFunctionEnum monitorFunction) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION); + if (gcs != null) { + try { + gcs.setState(monitorFunction.toString()); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorFunction' Exception occurred when updating capture-daemon channel", + logIdentifier, e); + } + } + } + + public void setMonitorEventCause(State state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error( + "{}: context='setMonitorEventCause' Exception occurred when updating capture-daemon channel", + logIdentifier, e); + + } + } + + } + + public void setMonitorEventMotion(State state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_MOTION_EVENT); + + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorEnabled' Exception occurred when updating capture-daemon channel", + logIdentifier, e); + } + } + + } + + public void setMonitorGeneralData(IMonitorDataGeneral monitorData) { + if (monitorData != null) { + setMonitorFunction(monitorData.getFunction()); + setMonitorEnabled(monitorData.getEnabled()); + } + } + + public void setMonitorEnabled(boolean state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_ENABLED); + + try { + if (gcs != null) { + gcs.setState(state); + } + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorEnabled' Exception occurred when calling setMonitorEnabled", + logIdentifier, e); + + } catch (Exception ex) { + logger.error( + "{}: context='setMonitorEnabled' tag='exception' Exception occurredwhen calling setMonitorEnabled", + logIdentifier, ex); + } + + } + + public void setMonitorRecording(boolean state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_RECORD_STATE); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorRecortding' Exception occurred", logIdentifier, e); + } + } + + } + + public void setMonitorEventState(boolean state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_STATE); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorEventState' Exception occurred", logIdentifier, e); + } + } + } + + public void setMonitorForceAlarmInternal(boolean state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_FORCE_ALARM); + if (gcs != null) { + try { + gcs.setState(state, true); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorForceAlarmInternal' Exception occurred", logIdentifier, e); + + } + } + + } + + public void setMonitorForceAlarmExternal(boolean state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_FORCE_ALARM); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorForceAlarmExternal' Exception occurred", logIdentifier, e); + + } + } + + } + + public void setMonitorEventData(IZoneMinderEventData eventData) { + // If it is set to null ignore since set back to idle from alarm is handled internally + if (eventData == null) { + return; + } + + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_EVENT_CAUSE); + if (gcs != null) { + try { + if (!activeEvent.equals(eventData)) { + activeEvent = eventData; + gcs.setState(eventData.getCause()); + } + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorEventData' Exception occurred", logIdentifier, e); + } + } + } + + public void setMonitorDetailedStatus(ZoneMinderMonitorStatusEnum status) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS); + if (gcs != null) { + try { + gcs.setState(status.toString()); + + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorDetailedStatus' Exception occurred", logIdentifier, e); + + } + } + } + + public void setMonitorCaptureDaemonStatus(State state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_CAPTURE_DAEMON_STATE); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorStillImage' Exception occurred", logIdentifier, e); + + } + } + } + + public void setMonitorAnalysisDaemonStatus(State state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_ANALYSIS_DAEMON_STATE); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorAnalysisDaemonStatus' Exception occurred", logIdentifier, e); + + } + } + } + + public void setMonitorFrameDaemonStatus(State state) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_FRAME_DAEMON_STATE); + if (gcs != null) { + try { + gcs.setState(state); + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorFrameDaemonStatus' Exception occurred", logIdentifier, e); + + } + } + } + + public void setMonitorFrameDaemonStatus(String status) { + getStringAsStringState(status); + } + + public void setMonitorStillImage(ByteArrayOutputStream baos) { + GenericChannelState gcs = getChannelStateHandler(ZoneMinderConstants.CHANNEL_MONITOR_STILL_IMAGE); + if (gcs != null) { + try { + if (baos != null) { + gcs.setState(baos); + } else { + gcs.setState(UnDefType.UNDEF); + } + } catch (UnsupportedDataTypeException e) { + logger.error("{}: context='setMonitorStillImage' Exception occurred", logIdentifier, e); + + } + } + + } + + public void setMonitorVideoUrl(String url) { + channelVideoUrl = new StringType(url); + } + + public State getVideoUrl() { + return channelVideoUrl; + } + + @Override + public void onChannelChanged(ChannelUID channelUID) { + switch (channelUID.getId()) { + case ZoneMinderConstants.CHANNEL_MONITOR_ENABLED: + case ZoneMinderConstants.CHANNEL_MONITOR_FUNCTION: + case ZoneMinderConstants.CHANNEL_MONITOR_DETAILED_STATUS: + case ZoneMinderConstants.CHANNEL_MONITOR_FORCE_ALARM: + recalculate(); + break; + } + + } + + @Override + protected void recalculate() { + try { + ZoneMinderMonitorFunctionEnum monitorFunction = getMonitorFunction(); + ZoneMinderMonitorStatusEnum monitorStatus = getMonitorDetailedStatus(); + + // Calculate based on state of Function + switch (monitorFunction) { + case NONE: + case MONITOR: + alarmedFunction.set(false); + recordingFunction.set(false); + break; + + case MODECT: + alarmedFunction.set(true); + recordingFunction.set(true); + break; + case RECORD: + alarmedFunction.set(false); + recordingFunction.set(true); + break; + case MOCORD: + alarmedFunction.set(true); + recordingFunction.set(true); + break; + case NODECT: + alarmedFunction.set(false); + recordingFunction.set(true); + break; + default: + recordingFunction.set((activeEvent != null) ? true : false); + } + logger.debug( + "{}: Recalculate channel states based on Function: Function='{}' -> alarmState='{}', recordingState='{}'", + logIdentifier, monitorFunction, alarmedFunction, recordingFunction); + + // Calculated based on detailed Monitor Status + switch (monitorStatus) { + case IDLE: + alarmedDetailedState.set(false); + recordingDetailedState.set(false); + break; + + case PRE_ALARM: + case ALARM: + case ALERT: + alarmedDetailedState.set(true); + recordingDetailedState.set(true); + break; + + case RECORDING: + alarmedDetailedState.set(false); + recordingDetailedState.set(true); + break; + case UNKNOWN: + default: + alarmedDetailedState.set(false); + recordingDetailedState.set(false); + break; + + } + logger.debug( + "{}: Recalculate channel states based on Detailed State: DetailedState='{}' -> alarmState='{}', recordingState='{}'", + logIdentifier, monitorStatus.name(), alarmedDetailedState, recordingDetailedState); + + if (monitorStatus == ZoneMinderMonitorStatusEnum.IDLE && !alarmedDetailedState.get() + && activeEvent != null) { + activeEvent = null; + } + + updateEventChannels(); + + // Now we can conclude on the Alarmed and Recording channel state + setMonitorRecording((recordingFunction.get() && recordingDetailedState.get() && isEnabled())); + setMonitorEventState((alarmedFunction.get() && alarmedDetailedState.get() && isEnabled())); + + switch (getMonitorDetailedStatus()) { + case UNKNOWN: + setMonitorEventCause(UnDefType.UNDEF); + setMonitorEventMotion(UnDefType.UNDEF); + break; + + case PRE_ALARM: + case ALARM: + case ALERT: + if (activeEvent != null) { + setMonitorEventCause(new StringType(activeEvent.getCause())); + setMonitorEventMotion( + activeEvent.getCause().equalsIgnoreCase("motion") ? OnOffType.ON : OnOffType.OFF); + } else { + setMonitorEventMotion(OnOffType.OFF); + } + break; + + case IDLE: + case RECORDING: + default: + setMonitorEventCause(new StringType("")); + setMonitorEventMotion(OnOffType.OFF); + break; + } + } catch (Exception ex) { + logger.error("{}: context='recalculate' Exception occurred", logIdentifier, ex); + } finally { + isDirty = false; + } + } + + protected State getChannelByteArrayAsRawType(ByteArrayOutputStream image) { + State state = UnDefType.UNDEF; + try { + if (image != null) { + state = new RawType(image.toByteArray(), "image/jpeg"); + } + + } catch (Exception ex) { + logger.error("{}: Exception occurred in 'getChannelByteArrayAsRawType()'", logIdentifier, ex); + } + + return state; + } + + private void updateEventChannels() { + switch (getMonitorDetailedStatus()) { + case UNKNOWN: + setMonitorEventCause(UnDefType.UNDEF); + setMonitorEventMotion(UnDefType.UNDEF); + break; + + case PRE_ALARM: + case ALARM: + case ALERT: + if (activeEvent != null) { + setMonitorEventCause(new StringType(activeEvent.getCause())); + setMonitorEventMotion( + activeEvent.getCause().toLowerCase().contains("motion") ? OnOffType.ON : OnOffType.OFF); + } + break; + + case IDLE: + case RECORDING: + default: + setMonitorEventCause(new StringType("")); + setMonitorEventMotion(OnOffType.OFF); + break; + } + } +} \ No newline at end of file