From 623eb57f5ed6953734afc6873d0ff5d0abad01c1 Mon Sep 17 00:00:00 2001 From: Christian Niessner Date: Fri, 11 Sep 2020 14:51:32 +0200 Subject: [PATCH] [tacmi] Initial contribution tacmi binding (#7768) Migrated from the openHAB 1 version. Signed-off-by: Christian Niessner --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.tacmi/.classpath | 49 ++ bundles/org.openhab.binding.tacmi/.project | 23 + bundles/org.openhab.binding.tacmi/NOTICE | 20 + bundles/org.openhab.binding.tacmi/README.md | 264 ++++++++++ .../doc/images/channel-object-details.png | Bin 0 -> 55127 bytes .../doc/images/operation-mode-values.png | Bin 0 -> 42437 bytes .../images/sample-with-heating-circuit.png | Bin 0 -> 38983 bytes bundles/org.openhab.binding.tacmi/pom.xml | 25 + .../src/main/feature/feature.xml | 9 + .../tacmi/internal/TACmiBindingConstants.java | 55 ++ .../internal/TACmiChannelTypeProvider.java | 56 ++ .../tacmi/internal/TACmiHandlerFactory.java | 81 +++ .../tacmi/internal/TACmiMeasureType.java | 85 +++ .../binding/tacmi/internal/coe/PodData.java | 38 ++ .../tacmi/internal/coe/PodDataOutgoing.java | 85 +++ .../tacmi/internal/coe/PodIdentifier.java | 67 +++ .../coe/TACmiChannelConfiguration.java | 48 ++ .../coe/TACmiChannelConfigurationAnalog.java | 57 ++ .../coe/TACmiChannelConfigurationDigital.java | 52 ++ .../internal/coe/TACmiCoEBridgeHandler.java | 248 +++++++++ .../internal/coe/TACmiConfiguration.java | 35 ++ .../tacmi/internal/coe/TACmiHandler.java | 405 ++++++++++++++ .../tacmi/internal/message/AnalogMessage.java | 116 ++++ .../tacmi/internal/message/AnalogValue.java | 37 ++ .../internal/message/DigitalMessage.java | 104 ++++ .../tacmi/internal/message/Message.java | 157 ++++++ .../tacmi/internal/message/MessageType.java | 27 + .../tacmi/internal/schema/ApiPageEntry.java | 86 +++ .../tacmi/internal/schema/ApiPageParser.java | 494 ++++++++++++++++++ .../tacmi/internal/schema/ChangerX2Entry.java | 71 +++ .../internal/schema/ChangerX2Parser.java | 250 +++++++++ .../schema/TACmiSchemaConfiguration.java | 49 ++ .../internal/schema/TACmiSchemaHandler.java | 292 +++++++++++ .../resources/ESH-INF/binding/binding.xml | 10 + .../main/resources/ESH-INF/thing/bridge.xml | 11 + .../resources/ESH-INF/thing/thing-types.xml | 172 ++++++ bundles/pom.xml | 1 + 39 files changed, 3585 insertions(+) create mode 100644 bundles/org.openhab.binding.tacmi/.classpath create mode 100644 bundles/org.openhab.binding.tacmi/.project create mode 100644 bundles/org.openhab.binding.tacmi/NOTICE create mode 100644 bundles/org.openhab.binding.tacmi/README.md create mode 100644 bundles/org.openhab.binding.tacmi/doc/images/channel-object-details.png create mode 100644 bundles/org.openhab.binding.tacmi/doc/images/operation-mode-values.png create mode 100644 bundles/org.openhab.binding.tacmi/doc/images/sample-with-heating-circuit.png create mode 100644 bundles/org.openhab.binding.tacmi/pom.xml create mode 100644 bundles/org.openhab.binding.tacmi/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiMeasureType.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodData.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodDataOutgoing.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodIdentifier.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfiguration.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationAnalog.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationDigital.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiCoEBridgeHandler.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiConfiguration.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiHandler.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogMessage.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogValue.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/DigitalMessage.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/Message.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/MessageType.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java create mode 100644 bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/bridge.xml create mode 100644 bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index f6a3d79739bd6..35e05ed924608 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -217,6 +217,7 @@ /bundles/org.openhab.binding.mpd/ @stefanroellin /bundles/org.openhab.binding.synopanalyzer/ @clinique /bundles/org.openhab.binding.systeminfo/ @svilenvul +/bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis /bundles/org.openhab.binding.tado/ @dfrommi /bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag /bundles/org.openhab.binding.telegram/ @ZzetT diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index d3786db8ddfd4..48beca13eb0cb 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1076,6 +1076,11 @@ org.openhab.binding.systeminfo ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.tacmi + ${project.version} + org.openhab.addons.bundles org.openhab.binding.tado diff --git a/bundles/org.openhab.binding.tacmi/.classpath b/bundles/org.openhab.binding.tacmi/.classpath new file mode 100644 index 0000000000000..39abf1c5e9102 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/.classpath @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.tacmi/.project b/bundles/org.openhab.binding.tacmi/.project new file mode 100644 index 0000000000000..edf721d9dbfd1 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.tacmi + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.tacmi/NOTICE b/bundles/org.openhab.binding.tacmi/NOTICE new file mode 100644 index 0000000000000..59b4d27a4ab2f --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/NOTICE @@ -0,0 +1,20 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + +== Third-party Content + +attoparser +* License: Apache 2.0 +* Project: https://www.attoparser.org +* Source: https://github.com/attoparser/attoparser diff --git a/bundles/org.openhab.binding.tacmi/README.md b/bundles/org.openhab.binding.tacmi/README.md new file mode 100644 index 0000000000000..65fc7391dc2b2 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/README.md @@ -0,0 +1,264 @@ +# TA C.M.I. Binding + +This binding makes use of the CAN over Ethernet feature of the C.M.I. from Technische Alternative. +Since I only have the new UVR16x2, it has only been tested with this controller. + +The binding supports two ways to interact with the C.M.I. and all devices connected to the C.M.I. via the CAN bus. +These modes are: + + +Via a "Schema API Page" + * Read values from output nodes + * Change values for controllable nodes + +CoE (CAN over Ethernet) Connection + * Receive data from analog CAN-outputs defined in TAPPS2 + * Receive data from digital CAN-outputs defined in TAPPS2 + * Send ON/OFF to digital CAN-inputs defined in TAPPS2 + * Send numeric values to analog CAN-inputs defined in TAPPS2 + + +Depending on what you want to achieve, either the "Schema API Page" or the CoE way might be better. +As rough guidance: Anything you want to provide to the TA equipment it has to work / operate with the CoE might be better. +If you plan things mainly for user interaction the "Schema API Page" might be better. + + +## Prerequisites + +### Setting up the "Schema API Page" + +The "Schema API page" is a special schema page created via TA's *TA-Designer* application available as download on their web site. +This page just needs to exist and be deployed on the C.M.I. but it dosn't need to be linked by the other schema pages you are using to control your TA installation. + +All objects from this special 'API' page are automatically mapped as channels of this thing, so the labels of the objects on this page have to follow a specific schema. + +When adding objects to this page, the schema for the Object's *Pre-Text* field has to follow the schema ` : `. + +Maybe this screenshot shows it best: + +![screenshot-channel-object-details](doc/images/channel-object-details.png) + +The Text from the *Pre-Text* will be used to define the channel. +The first word *tempCollector* (highlighted in the screenshot) will be used as channel name, so it has to be unique. +Everything else till the final *:* will be used as channel description. +Be sure to have at least 2 words in the *Pre-Text* as we need both - the channel name and a description. +The binding will log an error otherwise. +Also keep in mind: for the channel name we have to adhere to the openHAB channel name conventions - so just use letters and numbers without any special sings here. +The type of the channel will be automatically determined by the type of the object added. +Also don't forget the final colon - this is the separator between the label and the value. +Without the colon the parser couldn't build up a working channel for this value. + +The first sample is a sensor reading, but also the 'operation mode' of a heating circuit could be added: + +![screenshot-sample-with-heating-circuit](doc/images/sample-with-heating-circuit.png) + +In this screenshot you also see the schema page id - the parenthesized number on the bottom page overview, in this sample 4. + +### CoE Configuration + +#### Configure CAN outputs in TAPPS2 + +You need to configure CAN outputs in your Functional data on the UVR16x2. +This can be done by using the TAPPS2 application from TA. Follow the user guide on how to do this. + +#### Configure your CMI for CoE + +Now follow the User Guide of the CMI on how to setup CAN over Ethernet (COE). +Here you will map your outputs that you configured in the previous step. +This can be accomplished via the GUI on the CMI or via the coe.csv file. +As the target device you need to put the IP of your openHAB server. +Don’t forget to reboot the CMI after you uploaded the coe.csv file. + +## Supported Bridge and Things + +* TA C.M.I. schema API connection - Thing + +This thing reflecting one of our 'schema API page' as defined in the prerequisites. +This thing doesn't need the bridge. +Multiple of these pages on different C.M.I.'s could be defined within a openHAB instance. + +* TA C.M.I. CoE Bridge + +In order to get the CAN over Ethernet (COE) envionment working a `coe-bridge` has to be created. +The bridge itself opens the UDP port 5441 for communication with the C.M.I. devices. +The bridge could be used for multiple C.M.I. devices. + +* TA C.M.I. CoE Connection - Thing + +This thing reflects a connection to a node behind a specific C.M.I.. +This node could be every CAN-Capable device from TA which allows to define an CAN-Input. + +## Discovery + +Autodiscovering is not supported. We have to define the things manually. + +## Thing Configuration + +### TA C.M.I. schema API connection + +The _TA C.M.I. Schema API Connection_ has to be manually configured. + +The thing has the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Accepted values | +|-------------------------|--------------|---------------------------------------------------------------------------------------------------------------|------------------------| +| C.M.I. IP Address | host | Host name or IP address of the C.M.I | host name or ip | +| Username | username | Username for authentication on the C.M.I. | string with username | +| Password | password | Password for authentication on the C.M.I. | string with password | +| API Schema ID | schemaId | ID of the schema API page | 1-256 | +| Poll Interval | pollInterval | Poll interval (in seconds) how often to poll the API Page | 1-300; default 10 | + +This thing doesn't need a bridge. Multiple of these things for different C.M.I.'s could be defined within a openHAB instance. + +### TA C.M.I. CoE Connection + +The _TA C.M.I. CoE Connection_ has to be manually configured. + +This thing reflects a connection to a node behind a specific C.M.I.. This node could be every CAN-Capable device from TA which allows to define an CAN-Input. + +| Parameter Label | Parameter ID | Description | Accepted values | +|-------------------------|-----------------|---------------------------------------------------------------------------------------------------------------|------------------------| +| C.M.I. IP Address | host | Host name or IP address of the C.M.I | host name or ip | +| Node | node | The CoE / CAN Node number openHAB should represent | 1-64 | + +The thing has no channels by default - they have to be added manually matching the configured inputs / outputs for the related CAN Node. Digital and Analog channels are supported. Please read TA's documentation related to the CAN-protocol - multiple analog (4) and digital (16) channels are combined so please be aware of this design limitation. + +## Channels + +### TA C.M.I. schema API connection + +The channels provided by this thing depends on the configuration of the "schema API page". +All the channels are dynamically created to match it. +Also when the API Page is updated, the channels are also updated during the next refresh. + +### TA C.M.I. CoE Connection + +Some comments on the CoE Connection and channel configuration: +As you might already have taken notice when studying the TA's manual, there are always a multiple CoE-values updated within a single CoE-message. +This is a design decision made by TA. +But this also means for CoE-Messages from openHAB to TA C.M.I. we have to send multiple values at once. +But due to OH's design there is no default restore of previous values out of the box. +So after OH startup the _output thing channels_ are either initialized with it's configured default value or flagged as 'unknown' until the first update on the channel happens. +You could either use some 'illegal' value as initial value and use _CoE Value Validation_ on the TA side to detect invalid values. +An other option would be to use only every 4th analog and 16th digital channel if you only need a few channels. +Additionally you could use [OH's persistence service](https://www.openhab.org/docs/configuration/persistence.html#restoring-item-states-on-restart) and it's option to [restore the item states](https://www.openhab.org/docs/configuration/persistence.html#restoring-item-states-on-restart) during OH startup. +As this only restores the item states you have to write a rule issuing _postUpdates_ on the items with the item's current value so the channel for the binding is updated. + +Supported channels for the CoE connection are: + +| Channel | Type | Description | +|-----------------|-------------|----------------------------------------------------------------------| +| coe-digital-in | Switch (RO) | Digital input channel for digital state data received from the node | +| coe-digital-out | Switch | Digital output channel for digital state data sent to the node | +| coe-analog-in | Number (RO) | Analog input channel for numeric values received from the node | +| coe-analog-out | Number | Analog output channel for numeric values sent to the node | + +Each channel has it's own set of configuration parameters. +Here a list of possible parameters: + +Channel's `coe-digital-in` and `coe-analog-in`: + +| Parameter Label | Parameter ID | Description | Accepted values | +|-------------------------|--------------|---------------------------------------------------------------------------------------------------------------|------------------------| +| Output | output | C.M.I. Network Output | 1-64 | + +Channel `coe-digital-out`: + +| Parameter Label | Parameter ID | Description | Accepted values | +|-------------------------|--------------|---------------------------------------------------------------------------------------------------------------|-------------------------| +| Output | output | C.M.I. Network Output | 1-64 | +| Initial Value | initialValue | Initial value to set after startup (optional, defaults to uninitialized) | true (on) / false (off) | + +Channel `coe-analog-out`: + +| Parameter Label | Parameter ID | Description | Accepted values | +|-------------------------|--------------|---------------------------------------------------------------------------------------------------------------|-------------------------| +| Output | output | C.M.I. Network Output | 1-64 | +| Measurement Type | type | Measurement type for this channel (see table below) | 0-21 | +| Initial Value | initialValue | Initial value to set after startup (optional, defaults to uninitialized) | floating point numeric | + +The binding supports all 21 measure types that exist according to the TA documentation. +Unfortunately, the documentation is not consistent here, so most of the types are supported only by generic names. +The known measure types are: + +| id | type | description | +|--------|---------------|-----------------------------------------------| +| 1 | Temperature | Tempeature value. Value is multiplied by 0.1 | +| 2 | Unknown2 | | +| 3 | Unknown3 | | +| 4 | Seconds | | +| 5...9 | Unknown5..9 | | +| 10 | Kilowatt | | +| 11 | Kilowatthours | | +| 12 | Megawatthours | | +| 13..21 | Unknown | | + + +## Full Example + +As there is no common configuration as everything depends on the configuration of the TA devices. +So we just can provide some samples providing the basics so you can build the configuration matching your system. + +Example of a _.thing_ file: + +``` +Thing tacmi:cmiSchema:apiLab "CMIApiPage"@"lab" [ host="192.168.178.33", username="user", password="secret", schemaId=4 ] +Bridge tacmi:coe-bridge:coe-bridge "TA C.M.I. Bridge" +{ + + Thing cmi cmiTest "Test-CMI"@"lab" [ host="192.168.178.33", node=54 ] { + Channels: + Type coe-digital-in : digitalInput1 "Digital input 1" [ output=1 ] + Type coe-digital-out : digitalOutput1 "Digital output 1" [ output=1, initialValue=true] + Type coe-analog-in : analogInput1 "Analog input 1" [ output=1 ] + Type coe-analog-out : analogOutput1 "Analog output 1" [ output=1, type=1, initialValue=22 ] + } + +} +``` + +Sample _.items_-File: + +``` +# APIPage-items +Number TACMI_Api_tempCollector "Collector temp [%.1f °C]" {channel="tacmi:cmiSchema:apiLab:tempCollector"} +String TACMI_Api_hc1OperationMode "Heating Curcuit 1 Operation Mode [%s]" {channel="tacmi:cmiSchema:apiLab:hc1OperationMode"} + + +# COE-items +Number TACMI_Analog_In_1 "TA input value 1 [%.1f]" {channel="tacmi:cmi:coe-bridge:cmiTest:analogInput1"} +Number TACMI_Analog_Out_1 "TA output value 1 [%.1f]" {channel="tacmi:cmi:coe-bridge:cmiTest:analogOutput1"} +Switch TACMI_Digital_In_1 "TA input switch 1 [%s]" {channel="tacmi:cmi:coe-bridge:cmiTest:digitalInput1"} +Switch TACMI_Digital_Out_1 "TA output switch 1 [%s]" {channel="tacmi:cmi:coe-bridge:cmiTest:digitalOutput1"} +``` + +Sample _.sitemap_ snipplet + +``` +sitemap heatingTA label="heatingTA" +{ + Text item=TACMI_Api_tempCollector + Switch item=TACMI_Api_hc1OperationMode mappings=["Zeit/Auto"="Auto", "Normal"="Operating", "Abgesenkt"="lowered", "Standby/Frostschutz"="Standby"] + + Text item=TACMI_Analog_In_1 + Setpoint item=TACMI_Analog_Out_1 step=5 minValue=15 maxValue=45 + Switch item=TACMI_Digital_In_1 + Switch item=TACMI_Digital_Out_1 +} +``` + +## Some additional hints and comments + +Some additional hints and comments: + +You might already have noticed that some state information is in German. +As I have set the `Accept-Language`-Http-Header to `en` for all request and found no other way setting a language for the schema pages I assume it is a lack of internationalization in the C.M.I. +You could circumvent this by creating map files to map things properly to your language. + +If you want to see the possible options of a multi-state field you could open the *schema API page* with your web browser and click on the object. +A Popup with an option field will be shown showing all possible options, like in this screenshot: + +![screenshot-operation-mode-values](doc/images/operation-mode-values.png) + +Please be also aware that there are field having more 'state' values than options, i.E. a manual output override: It has 'Auto/On', 'Auto/Off', 'Manual/On', 'Manual/Off' as state, but only 'Auto', 'Manual/On' and 'Manual/Off' as updateable option. +You only set it to 'Auto' and the extension On/Off is added depending on the system's current state. diff --git a/bundles/org.openhab.binding.tacmi/doc/images/channel-object-details.png b/bundles/org.openhab.binding.tacmi/doc/images/channel-object-details.png new file mode 100644 index 0000000000000000000000000000000000000000..b98783c9c205e2a69ebc5eab118284e5970a3736 GIT binary patch literal 55127 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.tacmi/doc/images/operation-mode-values.png b/bundles/org.openhab.binding.tacmi/doc/images/operation-mode-values.png new file mode 100644 index 0000000000000000000000000000000000000000..5628af42093750e9e5e062f5461c5f324244d6fb GIT binary patch literal 42437 zcmeEtby$_#)-T-+A|l=0-Q6W14YGhmH;WFX1(fcT2I(#prMnU77HN?Z5WN%kKIiOn z&$-|IzUR63zq9ruoAth9jydMY-xyP)v@{g4F-S4s;NY;8mE^SH;NZEz?+0io;3rvq z0y`YsLsdUr1DH0%o66PQ#oEphN(J+Eg;GI%?5yG7d={Fr=2A@B66JqEdeh<9M}kfE zeYrl5OAL%8TIrl+StmUnX}`}x6ALHR@as~t{|D}`6s>}&@|h*~<{$Ts9p9a*H^u#0 zxSgK&`Ss=(^5u`Qz@JM8R{?{kk0z4hI#Yjs-xJ9zy}WN0*m`p1p_3KyJKP-kQX8taFQ?f&{A4L{rG%#ArAA*F5$^)Moop}Y25wQ%N@e? zVIzq=!GP0l%GR^()2|;pe8crE0VKw?4c)aBWXQnX)S-sTXg16E(}S|`zJKa`*}ELHc<9ouGCq7#=FRunXS=j>MDq5lW>AKQ zuH%>;x6M0;-qttYG9gUv*GZ*l&qc>ui#y)eJQ@GGh#MvUn&1O0t&Qh{bAMq}!jivp z_`{v|9#J)M1nwo3Hi&Hafs81d2>7-0;al|$y*C?V`v&M*Hd_*MlFy&14sxRGTB6No zZB*#5zR_6<{K%O1RstmpO%6kt$U;^fzmkOTO2>je(hq~VB-x#%Jaa~SoVq{TeT*J+ zs~xYVC{dNUsd37otga-}p}eViA#1T-Jf zjZ-CASf=Xqph1?#N;g+5E-BhpqqCIHTQ)vFy(e5aaVtn;gSLFxdP`-mDkkXCL-Dwb zzbSanq0Q@w`u*aQQLy2!nF+D{BUv){MTf&rVKG{U9Q&{`0seTA(l16RHX6HZD=JnB z%nYSQLt==!Ez}&Oj*W$`HNtw)PH~1BuPE6A%pCajHTRUwC&RFj7+=~Me4S5pU_9bi zJ-Dh&O`^Em@-~Q!n82`;P*f#&U*~2##kke>oF!2A)kl2w5Dya{ z#7{-CIoECve!Mt)og>{cZE{k~4BILD=;uZIc{awBk*H-%L|tdF#baepm{U>J!|GFS zmCvi?2Gei&Dx`(J#-p)XshmcRDAqeDgWU`_4+0t1r8?J_mud&B*R2Qrd+Ln}AfyOh z^o&CSS>c7=LcE_5{8GhtV%5@^lS(dXgF14y9rmflX~VSQUcwGY&89%>|>@&?pX_d?&H{a_5M4@t{O;ygYHaIR%1iLrlm_pJ6 z3K&(9iBXZ>b>8+2)T!An;Si0+yoEiyvYgpkjTaJa!X_&^t-Q(RNR`^%x{v44kOeiZ z<@oTDdR8K9Vx-S^)f@?Dx4ScJA`Nf@Ty|aw*m#yEjzZ?l#+S}*uNK7IoI7OAorW1u zdQp2n6&)FCzImmkF+7AL#TQP0{@_{4fzigplgGm&zec2*_>L{_A@BrjB)@k&Prvz6 zNadwM`_kjZPzr+i+(ow5!^h_LbUdo93#wuIl5QVOGqXz2CN>=3Rc;UG-=3vwm=JEU zbo9_)&=)iFlLl6QbWgZ&o#hgUCQ;iGBbE|^aQL{ij zXqtQWr8;pYIIIY2t{d*nuyoW%y8hXyi2cl~*T}^5g^KMffybO71gLC0PBbW8WoT6d znlYkq_riN+CBkGz&ur4>$G7_iI9>{zew@Ipkhhy+;Cz$V6)=F|hd@Au0;f(&9}h8q z6z;(jQq5r9%p9MnDTPDfW53(fxC!^EM|Mjd1s5K+u4!V^imm#1!V-~5;802{n2or; zLXTt$$;M#FK_3UL4P{eExb`_R5<-^I`Y%(nn)gyMMGY}S#}}{&gD)>Eh?1dGBnG+t zJ?~p;(yj-Jd1?d8=((PuwLV5*kEc(j!+rMAT!zpuS1F8g^K=Ve1^zV=Z63FIgG_FG z47K_!eT;O6U16|-Ry6%HoCNA;^=v}>{ADlVM^_pOKlC5i1iz@YhGtA4$N65-M_LWy zSs&``#uR7ssjqNbtFTXVlQE0D_UuFGEK;jZ{Q}88l2IV4q<$%(Y^WD<8B3gPMnSk2 zq+y1@0SD!s#8wk1P-44>P&5Mvt#P+L+Q(nPcs?~!8y~}d4;ju+SbLmTaOjyKJMU}0 z;8qM}gj6{94OPckPc9@+VkzJ3#{Q$?a{?U-(Ut=)04ulB_^o-<;ZIZLXzVCaKFf!hCRZ7b%iMf*(JMx2N zGRWKNvwqY?2Q6=>F+wM4##O1DD%^NItiC={=T>RdV)1B~>IZz-cl5QVG(9gfgp}Bk0Oix4p}PSdDL z+Jh7ReS`K(Y+Q*~2Vz<3&rM+DXW9vb+6B^TXzU_b1IfDxX=#V3M`lM`FX=`4W6Tt0 zzL7)@&PXYYTe358(3x3MAlF4g84U2COwIh0%3bf#`wbPVN-dnm70NTG6tT=}sVe)O_PhYZ2$(8~O$m}}?PQ7K8?jCK@h8xzqm z8ng6MXN7#UN$Ik#NyE$(Ol`q}`;+q?`9JD}2M`%P>5pUIe5$6D_Lp`;ET>24i7r(A zcwyU*an&NW?PY>9g0RgOLp_C8?ve@bT#mYXyL2#S5HhcIAKpk#_k9T|Z$4*Xcb8nu z7Q-+^DSCj*_c>Z9vAz_;nuaNrHBO9mR`QcM0V|3xcsvs}d>%TgRwGk1xQf?Q5UfO* zw!}nQal*_L_;FhO*|^ahM{3u3#*e9;k6&~rdMeDb#nq0HkB)|@$vQ8z%g>>z3$|$X zkB1f87TXlrja`XZ;^i{aipwPPUa*`fc)Y+G@qAXet)ck@*p>s zC8-n~KPJuE4oMpB>{zUnEaa*V+rGry%N733d%1WdG;~M$ccerK*OEe8A!mVB97a%K z#Vhe-I1fZ+En3C!dLMoa;%7W8aNW0_0#d!&8|RA?@}V~}@sG!R%uY4mz7c`vsK1rR z{ak7qaJ){rDEL$)TrvFq%zfXMut%7oq_4|;@*HF$3PP=(RvBJWaN~TsPm!N6%0q#> zMEgk_zLwz|z7pGjasBF=mk;Z{DjZo^&q~PTk+i;8Y0CZK{OO(q@@jl?a)8|o&!g$) zsY-6~ugJ{eEcc=(=pQUntwOloT~Z(6IOJa|UmA!#v5o)Mr;4Q(51Gy2QRy)2jd2RL z-Gghx&p{McLHEhV=HvTXv^KqGpt;2P6KVK;<9)fN5wC?^gRjAshqxTleouZx-!Mo{ z<0rzAR`NL)>ZF;|mBSgr;i4uTPjpRANanudeflxOagjMPXZUd2Yr{)HGmV84tw5hz z)E559hL}IHM`iQFgnpWEg@ef(%wrb%6_G>7Lqsjt`<%Bac+E>S!k*^&aBoo0Ea)|) zW^=i>YwIHNHNVK(MtBH4*hqeC%)rQ`%VwpppEdm?(`OwwbNjE`pl7=|~HXBl70(DBy^`4~A6;gN^gTH@EJNd{e}J*)I;CtWn5`A1D!3N~aSY9^EU-9Lr=G z((y;fg5)95+IsmKlu7R)eBq!|HmwXH^2~a0RNBE{M=RcUc7#F-ACSXGb!Nn#*^ z+x>}OkJvfHQ}dfo7>A)DjeDa_{ejhi#*&yr*j9Qy-da4Nzy^xfx84ZqN8PRCk4NAm z-7G?P3rP?%z3#<)a+AR4=DHtpJJY`9M)yc(+;-^zpM|a~bKz2kBwyHjQAD?3Z5^Em zZq)%(otW4AVM%arqEJQ?L?XR?`!$n^|Ih@%x9k_AG|%UW8zgcI>D|0+-5Ky))c3>0L3Bn$gc7Nn}kLD=Wm5z@NBgclDvDIlH z<$?WuvcXs6K1i!Y&(ZlUKZH6biYKfwT|^1g#7j0MA=rPga;oq3w+LHT4M>Y-BOuK# zKf~lf-dW(+=bIfwIveT0eh@{1{E1dz{1xk?xJRYO3$(r3P7fP9(J?-35V(rezV?hv z(a!ji84!RVWt3#IDpIK=nqGoEsWRzLgk6t?8|sX1P`|PM)BoL*CTYp-k8Ao?>?GC$Fy~9MbXzyR*n(V4EoDEfahpWZQkYkE3HlZj>F)_SPcV z1P9?(d0it!`h6K&iKhEuSs`MP?fY_C*I?QA1K#Y012MXDgU3jpts2Z;2HE061^dHi z`w*UVxlOSit5e%&mO&pCz2aZ zY`JZxyFmZGk$HnkuOTjU2R-|#-=#Csc7N-1soQvqf^A_s?k4*JTrAG`0?tumbJw29 zyF)jQPhn?KYv1DM?fButW6;Crvo92UT=m2ir0G$*XBKB6^7n_dWg}D%;L8)#GV9~Z z95%G_`zQ$C`4t_0ZCnl#?VpxT7>WyVTfF3?`DGtqdg_PvgNBSF?5UziTYZZ|iwMUd zBK*wAvEOIG%FcG$5{*0JC>&B}j1((qZY#mNW* zj-=O>(GK4|M?1gEoaB>VQa|0h>L>P`!tGp5i@G7hrv8ptjc44`%lPL6XVvQCYxa~- zV{K#xyitZu{r(}&LuFts$eNUA9&tD;zB1TYdk#d2V>!CO`Fa*g&!rDx~%B1$E0_dSeN-GveNZwTyiVyW{4z{VOtal zsTPsUQK`~~FT)0YWr4r{nc;LenRU;*K%dY_zYn$igQUoN{&uROMSdlhebz~N zDylr5rCO~Jc|rU156yVuKhZ4lSXv(}5WM4mKWj4rFM#>nxEk3Bg(`*FS@715j!)zz zb-ZG`5keG)Lgm}67q6@n=!{ig;|)yFJdATJ5#4)F|3coAoE=*MdTe@`uE@vz8foNJ zV@bD8YR9?)1AC7f$}{S%qtOX?d0YbwWpblb1u&6Nqa*NGZtqSRVC-j?PT6y$QJ!CACAiLfTah=;UCcq&k+@5jx zL{0@Wy3)Me&_F9BB2L-D%Tz4-8C&XWyN1U@{^mBGyAHOcIP_27RR6Ft9g1SxuWVP3 zNr4;UnMIIBK@N+S+m(mmqF@k-h`hIbtZ0Q`4#S~ujAQ!&p{bmV!u;T7yPViH{N;TC zk?9sb+qaZVPoh%i7Ilg@a8Ep`!?Tf^)@ogAV!0WT?I-Se2B`4sls@iht$FH~Fp{z3 zg((dhxbpb>x=d=l%78TT8(C*bVH1W|q5__l8O#NPW3iEtiv1 zHu~v>m{;HcMQnDz`0MNhL#ez6j1QIEwAJpHF~Cj35Rj&^0wM(>+Q%4W85CQjc{Yw5 zXAXImYbD989W6GwJRhj|BV7nBPi*0@mp;{dO?X{p(n`anYA;3mxwI; zsMw|kGrx?o$05AizbZU@!dZPPcVvor%~^!wnOl8qlum=n)3PXNT`hHfL-JVkl>WuZLC>tgu_<|VLmi)QblBXjiOnSSCP{aGAk9|d#ULa=X(w+SWc+^w(`^iH1U}h zjPY&)T9kHGH#JDlBUzt6h6_|sC5onPH&;hdY04=$)Wk~3V?h6wZbIKDwK8Tov!6Yi z->lW-DHMs5i|Nq#HHPXn2dm797ljNl^G_$Q`|m!#OQW;hQ#Mvl#wcHRRcwF3_I5eL zwDbN)U8|z(X=kg{4Aq4PjS4$oAMpQdfkjYkL;C%viHTVF#igD;1bUu zySKn>Gx+_K%-iKPzK!s+%BM_WIEcLv(IBFnpGdFX7y9B_J;HCVE0ILqD(*<_Si2&) zKCTzHW+lWnja0oRB=(2ps|~^I-(9{<^?JQ;d9UxZt)>4gPJHa<#w9On2RG>a-ObXk z%cIfN^IvcMPrYxS9oX^LiHVfW9v)N7aIDXoXm~SPu5x~kB`KUR^Vj{NBAln z5GKxEXQ=e_s(C%edP9Q-kN7-a(K?lzQCZn1w#NVacfb4FhATTi$pY=iES_o7Lxy@pf(hg<7?t8 z>Z+FpP;b@``NlNpRo8tKzrvIi4uodx^C`x^r%9!f4htbwVJeJfsuE!))^4P{{E90Y z6lH?ZreeNjAcah7)sOoVwX>t?Tb;5XGZAY06UQ4%BD4d9(RQb&l#5=cNM9up_5Bu~ z*unAU70oRgZJ~WxMd7Gk%&bQ>If9ebjBp%^-700?I(-kWGIWSZ-XQzUEv*>c2nE7T z%{=~wA|!;=hmrjD=;X-U#^qP8-T5V=*aPQ}T0%>iB8s zTKYLy3R}@gN??fjhyVsoP#A>D$H~#zL&Qg%<~Lsv@cZ3kP8zD;BrpeY8UuALDp?nI zC>0+E9|sq^ypNq14~+x{m6*GgwTQNy!k;X_H*p$U7|d0KlhfPVo5P!z!^Pc(lUrC= zn3Ic#lZS^LP_TRWI>R77?9LvvcP#$kAqVxabhmSb*||7V-SLE2xOl?EX=uQEsy~^7 zx0dog;NhPe+mU35+Z7(V;I^4l8|vZW>23*?_kudZX#cvYtD~pKU-$I% zfZjd&Ew`hUH75}2_s)M^qo}N|_4hS*YP7L)a{ayHj{2{ZR+fLKbMiideZ= z+F6PG{#A%e2*StBZ_UnaDagmp$ICCkF2pCm&(0-a#cj!D!No6ZZTS~g%FZ4zh_fa1 zjul|eVF&m?d3bp&ArJ^VACw#X;1>ccAi$RdA-q;lL2F(?s5RGLSZKQ2fk=Qj{#B|w zR#t$OB@dU7r7#aKJHIs_KRX{6FAuvg7Zk$I&&$OnY{}0pB*-oJo7J5iM5MKp#c6mr z{$T$5iIyV-X6@qcBu=9av82+}`3J48ofA|C2D#H3H@~m|uP~P&KQ|ALAP+zHKZx|8 z?jFF)@0fCPad7kfeq&`Rq5xPzfVbH>L2RI$uFf{UU)S!PbxI6!`RFL}* zdH(U%@Go!q`Twp85m|RA1m@zd>*C@lPID(d)!nwgsMTFi z{u!1s5F5_lU&T28E8>6Qq+{#i?fl>4^AFO$bCGt3dAqnfXu4}!*h4L0|23a~NBnnA z+8`l%z}$V6|1XF7-{{2t@M$Hm*2UfTFZ=62-TwIYN7iz*`>iS}s^6JH1Y&uoMR6Jr zh!@oAxA}k`|9E9-3vsrAf|UGcJpS#u-M{g5K4BpUudt9HyRfhz7dxMTwGcampUVok zDg?@F!DlUG@u&I!&fUYs8s-gghf3Rk2n2Qp@%P)#R7`)kDDyuH<82GQ^FJ;gF81G< zQkPp;gr7%*mz$M~M}&)u<`0uoiE-X#;=dhP>@HWStBd?qDPniIOhoz4V|6@TT^;S9 z?*FW;zd6tUf$lHs|8&&<+w#AX{jpls#nl%?yDd!9+xfrT{@)P(!9mT=5(JjZe8KOkEiN7!5$30r>O=jelX8V-6S^ zpjfCX%EA4*`|s7;(j@Q%-BroZ0}c+8@a_*hTxK>2c!&y9R+mTJLPNmkqx||#Q4|i2 z3Qk#0TGwac;H_^qRNwIO#?=3qclN>FjF_kXd3-Jp0a^x?G$C4;G%|I_SCLox_*Pm0 zZ<&PN$lz&xHH)}>hf#xv{D|6w=v)O0K?V&;R))Zj;&;OIJqGxsG4VY{0{@ThA-elSF*AV=fsU4z7G23J z@KI8e2A_oV5b&YlMn*=Kx42vO-yr`@{cp&BGyhla|4MGcrHgM8pu}80YaiCVj(Ke| zTyJ1#n6Jx%j}y7OyZeDFq@vM)ji$JV3u|bbCnqPzI7qWto0=dh+v9rgT!fXCRhQ*n z!6cKoxVY^???XZM;VwRPnfwXi)vWQINj_}TxpG~WBJJ`RBMEJlz4L3|;&YQnOhg0Q z?qS`Ksa?Lzl1Ei~gH}9YGSoY~f-1!X4h{|^5z^k?Ej#`|6a$0zTv&eo{_tGtNhvA! zfCvdnVY#^!FjM{1PhQd-X_jtolUK%Q!FXM#EjKn5tIK(Ysl6wg{a{y^rzgaaSTT;G zx4!bdH&KlIw=1z(`;^()$K$-qb61iTbNkssZe4agS*9+9dKGS41Nd-2qGIi+Bt}i= z%yGS4`}3BY>-l*@jBpuqa>t7|GWk+<21Oe!eDR=2sGUDVN$^YzV5Mn;A-+J}V&(&gplv-5M|!mWV;G`K-4 zf^M#|xqaKrzL60uaLDRvj_d1dgz3~lt2^+3xvRQi|d!Zfa-D(T&zW#pnaG5Q4VPI!vh9oPV z2lex8bp|7r4@Sqw8-RYqC~SOuNT&HRa&t?x%Z0WdgaKC|zJEVTjs^=Cmk=kCnK))- zY|O%t7%;4?uGVHFHZ(CQ>gdSI$asJejsUzC3kL^01%9SsW>H7dWc0GQn7$|(2*@`4 zE!meP#lp*rSbf^S)U>#w0+X3OLoH%u#mvvoFFiBUU1{hiTiP&iS`socIhEZlOBgI! zjz(ID?DI$lFWiflFB4RX%XC??B?8+A3z|CP`ZvXXe&5X9>D_c=U}VIX;Sv%Wn3^Jp zk_*Ytr!K9&PfT3PwqIbhGDP4XO%?X4vojBvf*=a1R5Q=o-2B;chc_=jzkIQFz7~^K zx$dX8{(?ZMx)qNv4(7Yh&iGQHKonregVoMFM*&rVtYTdjUlR&u`oO?I5K}f=%PlUH zZ6>j541`}SfbHsR#wox_gaWU@Qn$SsW)xJ^`8rF~q@*MeA$MVZd~Dww0sf3vj}*Y~j%4M4(@g6*{~Q-KS>f=Lg@vf5u2I@Zo5&1$Vf9z=|hZ*dxM< zXYgI%4bVB%)04k8fS8!L^`L(E%if+vKmge^pRn&YI$%#=VO|Ld4d6d@1}{M5mz0*G zM|R&G-o9yQV}s`0F9$?4I8zQRT!uPw4Fn0JX0Da3?Q@_=kLxo#`zF$GnP5Qt~urKk`cuuOdH8r^E(_an`p-xVtZubeJ!b&GGs56(M{N5hzng_+q zGWby=0XGK@&5{yrS1GdVdaEk>~u&077n)$=M~MdxVVk(imjTsK;m zWm`Eb=c9Yu3#cI*2L}(x7A1BX%P?B*RHk zqC*(=vamp4F1CDt21Ng4l$28j4DG-q&jkW&~d8K^Pzv( zJsF`QQjBme^=V~~_~Ce!Vo=tob2LPEh5gQ$ZY~aG(bnG-n$`G=g#Pjds{QiiYwKux zHR_&p)zA`$=;B_3WWz8c(`{yn?@)hGjp$ntgSqGg-Zm?h0ZW%(+K;w4Q&jL{dgHZG zR%~_*Rd$)X^$0ORfB}ya5fLF{og+R+MM0VHTrSsd!FzgfV92L#3B1N>c%CB*1c%~Q znzs8!4|-o;AJ|(96tia~c9m6Cpc*RE5yUF+a&u44NQEeHQX_%$f@o~0t(DT(zklMB zZNSzC3Y?*VfosnkOhg?-hRt{`)Yli}{RgFPZL5>>^SiCuK;AL6$3kS*US6r7geiJg z%-UC0R&sH1y`OKeF*G!UxVzUY`@0naPXNJDu2Tw%?OIP1Asa{7r6nfi2kZmzYvd!h-)&d9}r-s_G4$13)+7eF~sD6;xAT4eWK z%6ju=WNXWc(D!QEI%#@lhA1W`2IwY1jbU)x9b|gHAsbNbWb&USCxSD7{`{E>D-_ft z@Jo75j{KabOg^5fs_NYeP}>X*rRO}Yt*z5DGVB~4dwP1py8^wJR$JDk^GZE|ZLS@4 z9H&NGbG&QK8?^@RD@TLx-bQxrw~TMBT1=3yh1Si*M1&z%_y}gom^V%s*MFCoZf|c5 zQsu=cyi@I!n90RQz32~69LMDrj?45al$b$gFsk~DHgXj}K^n2Y}<=dHd-bDpsnzah-};vTya0rq?rfseNb9cYMpY4ZnSii= zYGCISbHS#$DJs2nM+e?>_WVHj_Ua^+VlvDz z2D6TUgGPD+^iqj~ZznswrUC-oq!H2rS-pJfiGy_cTF*fapd)bOV-#Hmi6=G|Cze8d z=<8RmRH*P()vP_RZ``>KONv-gwH+y_Um)AGPI^fUZP!nuyqu*j(%4tQ4QxoXqbbu2!$-p4DY zju)B<>0L>E;byxvuDhE7URsX&Opqu4jD`O|xBrTT|3J6@0-gU)KcFO|0Rmq^(_Ni& z6xc^{SZ1^R&{kkp!z(GN$&+E_?mmTXRyXTU>3_ftG)Z_auQFH|CxJ0RBqdPx0$5>z zsD&F1#I-!ydUn$jCL$q^&uYVHAElYnhCv+0t1;ZAF_7_dwM=(HuC9E?a>W4@fO>g- zj5735>L97c>q;0F2Zy7Rzfz z1tpU5=hsN!{H1Szf8t91;2N}YpSH;?8QhGMJVLs^f}y9cFAP8gh%7dCb~ljG7{R}R z0B{}1Gch^&VQDFQI$~#MC%CV?9)Qw*%BM?Fk@FZ{c|ma7HR0uynJZRY1)Td-@C!6svx-U+So_CtY!We{Cm>s<4WAETriE&Fj`rls2D zul7X%T1Qvduc2iw-bp`j6d2yvD9{lEpuJErE>WczAay0W5WEnT;t&YQ7@8S4RsW_z z>b6`}tQ^f9=7w}1taW<*499yB7nd}?gILItG7JT1a2`OcNn0X-Zah9?ffECeEqo3N z*e<9)$uE}~_|!dUWaj7QgP-%O(^ACgRO#hRc$%7;YTPwjK&HW%Wd+CK37qrK3UmYz zD6|*7v;`n!kTIsL)85{yEfTO0$B;y<`PrX-FsQYS4LLz{|0bEJ z7Zx-9or{7}pi!FB8ZHogaeiLc-oDp^ot>Wk6-MMA*0}u4zDcEAx4?u$)R!$q1@yD2 zsi}7`6jV;oXs``ifM|{4H!RmxD%J+}6t%mny?bS)rIGG!BX`?lg*MoXTV$3HqUoc7 zI3T+-*fPKfVbzju8%O{$0lffp1rj=ds&|oErAJE;bytQ!-)3(g4JtZN%cSj0)c7t= zhqdBxb79^Mdz~7?rrcn2O`IH!nx^LT^mO-Rp<>Pl$RCX!sAvF3YrsxG(+AF*k(mh& zKp4}T%{MkaE*iQ&G=y>DvxM$|>iXlmn@oN@W5gy1WQ{7lLIbwMK`Sx4X7nMPn9_Zb zhK2?W1JP-|oS%2qz6VhpBo-O8&pSH_+B~3h`Tkv~&1B4O4m9{dh$SN zIyrFz(gW4l?Re#lZo6w3H`8W=`U9%)rv@8b9UUEwnNy$_5$Z1o^`PHRR-p$q2kds& zq1DXY3Ea|h8DcP`sr)Lwu8y0LF0fb|T$u-h-xCLmrqK@)7S*4ns1%pebgkD~f|tx* z2cH0N23CCmIwp{g?xG3KW-L43fDNJ6V@G3Zw~??$Ynm?-33whe5k`?Vb81=|=)gop zhmV#!a;RQ`kOS}(l&ib83ZPoT^k!g2U}eUOwY4=W5a1>pihQKdov|DxX5oPET)^*u z&8pk?SQwoPpoRdPa0>{0B&z*r$^g_-hji!@0%i&_`R{YZ{huk&xi04H9UUpC?DCz# zL=FwYj-)C<4@3hZx#n&V0^qX9xwY|7kl@0x0usN{4Y^n8LlAA&))Am{0{#!&&K<^~ zJ`M6R&=+HDQJ@P?2ADXszzlzU&;^MKROT(9R0=-z%`cOM089X1{nX)IL#WT2HVn@N zT6h;1PSBo%=1277qdEgCb8|QVZ;y_w{rnaZM1cm^4Maev$&jH2I@0WGpN%NS?B|=# zMbmZwH~^2SGlv767-$jbQ&d3AJfNiX-cXHw@nQn_UlbuDXmmj^2|t?kXv>b=y3Kt2 z)@%n9m4lVHS+;z8SPF=Uh_R8it*u|=_`#eAklvxuo`FVo;l+*E{>{4w)6;|ommox9 z?{$KdG^_vQQ102d6;JS`_@R;-S}=q}iJ2PpM^QXaR@Y<(E@OZv;eW z+||3)Q6L5{M2l%gS3p|@Oi9RN%Cwlc`1!Y6+b+J$FD}La_zfJBBTbw`mlaH-ijp-H z>zCm#y`8~`BD%LAiMZpm?bn4jT?dY1+7!n)Rozb$Im<=&i;nblR}#}Xaj4TULWx~H z$Sp__O^cR7WL`_}2M3k=YYt?^tZt7r-F6}ebziWjsaMOy_pGB;Kz0MZ~IK_DLP1m7|TFK4N2MXN{X5 z;XqSb+uj~+mkFj3pbCRLZd9dEuIo;kP`wY*E(mW~S=kx;rqq;!Jkxs7N5B<04WSRXF+mbYbtRTzAN=1$eF)oh>dNh{;&vrsn28kU86{MMbj6Axpm;JJcNO zJ`ulY;X!;51``K37E8c)E?2voLpR&EoWIp1NF;8^<)G z9&49}K@9;8YUDG|^~y8=kV9sCQBPMS#~LJSP}x935*iw+f#!ST*UdHInFA(vcR4#N zi#Bh(0VFPL0Yj=glU;xa1m^>SE7pM4=C1p%LvgGsGvNReHZTtX zX%Hl8Fzf>3Sp@|JvJW#K*&Po{w3sGlXTu>RU{nZdGx=~;Ljxf-5~v_n4TfGvOUi7- zvmrQiEd-7qorjIU0qUC6rf8_>*t#% zLZCdyrkHoQT%0$v&dtv&gBdKCPL{{$vNS=u9-@FY;jSm!*&)9iCN3x~DFJGx%}`5J zqRWmTi;{cZYXN`(ID8#jOhN*FzXcZP_&~urUt<(B9zVsU+J$8Tzb$o+jEEr9_XL5Q zk+KY?RiI&t-G0;9C`Xg<3W(<3cA)0?>9FU>v6;c3To+VA;L#vGg4F=5b#)4Fnujs|Tp9dQh3B7sH$H8?gHcYwX@n=%P+O;rZLY^}u3f&>LrI^gJB z!oq`KY6lnsB`Z2KEw)stFA{{`;+lgQU2m_v;-v@p%cxd~U4H+vTbd`UdJoTQ<=3jmy!+3-qO2HX^~X{s~j%I>}g)mhK$GCA1y+r2$QX| z-heU(>L^W$O4AJIfp_Duv6f-$KCB_Yk-k&eJ)urUrvDjqHJJ_nDZ-(Rc?c28iorr$a6@|YrS;f~ay>9_=)e^;* z5hB*C*f%b&GnEjiXz)5TuW;HiSqPR~HFokhR({I5A+~yH>Q3iXhxdB|sklzVkJasJ zQdl}XbZ|PQ3d;fB@i>2x#QwOb%hy=qpKn9{+t(X^+_n6-$^UXQ^ViFh-#0T|)0Fr5 zkG9^whx5NUpiWWoEzn|RViNP9DyGXD2P4X{nVFelSqs4xAKG!>F%K>*+1Y}S5QI51 ze|E)KvUP_iHw(xwZTudQe4fac6}xO32ZM^o9^jfsik9?dJMD>vK-PACB!%B~V!BeE6#U#af8v=TcBapjSEHsH=O z&#^5U=$`!ii2n0nm2na~r#7*lh^U#rp21`izVq9+&?VO^s?;WFXt^#%H&^aEI_rii zZ02>&Uss9&n~n*2Zw~Pys!7zOAsw}oWb#3Q<} z&%>nBX-^zE516UzRF(@0_yAR@cw)p>csGAUB1*ni8TV)_^S&~e3Qx~tT#kn^rLXw2 zEPf!U$V~hu4`Zx4M`l_;Hu3)k3umU!#6z21CF0=XgKs&m61x%>-*PsF(7n@jik*(QBVmTF9-(P4ChglrDaY{>~#HO7iEmAyyT)!_B^6oa^aNv)FNK z-y2CX#s!5ipF#dmEN_L$cj6~SLpsvbXrcwL_7h4DrH*FL1=^4UQC{t%SWdD;@;uZ^%XnJ&X>4BEISJuj}iNpB69HZ44ytaJ)lJ6s16m!<< z{#buYEJuB*F=r1Abf)erUG*tG;py;;XS;n zv+7=+>Fo=2f9q&pFMN!FLBV5ij2gtsmN$Nv5&_J4KuX$G&j!#0C>>-Xu;-xG*%hKB zYCCN{aQNI@+w&$v+4W)M6*S+-;F;Kwrk`ZMid`>R^6sraLac2R>-{j2@R z5?_n*B_HP}F9b~_)x3FR7@j~hpN14p8?KzX*wnK5jDB!-UYL_rl(VsJ8({1d>^7;J zS83m=qc`(EC0UL_6U2tOuFjLSyzJYJjvP$g3%HJ=um1AlYv04j-TfEviBaJQls89M zZJzGUr$!a3*WDs6z8^>$<}oOLex%EMmM1*Z?b*@sy@fVr$(tcY?wig{d+WIrXOzM7 zwiKGY>lG}WtIWMRjM31lSt@G5s1z0YquY(Q0ed=m8~18_kZS^$9Y75Y_&lAro}fsv zUgZ+V>iy=MCM~se zV@Ka*X_;C0F=g+Ya3>Z>-diXC1xt)h7CSblyuL0uLzGpnCQ(2ArYR9p?Xcu(h%0FF zG1N}^wP#Eycr!oSYnt~1z9W%ZB4F-b81_q<;xXtgde}1qrBdyN@1cwRz>Q^R)RdPS zTLb8Nv2Xm5+LyBb_IClUiv5EIeBSL1s=mdD%Qjo@yM4P<-4BRm*>0>W%{79Z~B;GCvLDXGcsm8Ex{Dg$_ zN?qBy{mp8V^vya$ul-hVuo>Ga8Ko3ll5o6V0bY<>u}s9@63k^iEj>zZ`y*Ma5Bay?#`}V{yK8A-Va%qd+y!o zNVy!vCCQz+Hf-~d5I$TbNAhZjn>(kC_Jb#p=CC{>$+%Xb}?B?Y3@QL9jU$Z<|mfm z(eZZlBu65t33sXjLf+3KtlsFsmXgySKUXFCeB|9fWzu4-6$WvQ>Q@w!@b zmr?n8#4dvKa*7P0ISHxDJ4;R%_=5F5;VvzO<68BBAa;3I!8e7%w74OhbtLHA9soBS=Y+nc- z(iB0JqdhTYdC4sv)))pHJ-%r>s;4MPjS&lg1+!kl8oZgpOEAAx;}__*Fnwfac`TZf z=t60@G3EeDkVC}xxD_GPh(x#_Yco8SZ{7L{qQ4;E8I)MLmk;c=kwrisof*a=qpym# zzh`wJ-Gt6zA$*;s980Xq96`K%yNBnGnnad0=>C0%K+u%G$6Q1~H`HPowlwIs+-2lK z=f2$Zaf!}axxKJG2b(eQv)^^!{Zn)0XnXkjkp3LU`1Hk`Mz)x!2Eu@C)+g&*QAq&v zs||0}QA+gP|M?RWjDfi8EqkbWjCD>Ms}7|sdfVj>#jF`CJYy`U4RKx+`5af^oM1jZ z7g{xDI>AJpdUKMRvvfUADNL@fqFil_KZP{aY*=Ik%W(|aS?LbA_=@qrXgbTVs=B9* z3(_SaARt`=(%sVC-2&2a=25XUEyIX6~6a^IP#9 zudO{#j*FbxNK}t)5(VbGa9e^-FJ*ej!4Z<PHm#@f@MP@hLFT&Tdox%Bc-M(sP;>r(0w$ zq7bwnJM`_*!0)&WgLP>+5R-6GG!-Po`uFXz2kq_rTaM^2*;@7BHVMUGpITX=!eFZ_ zsP~8KE{KtW5uc=Xywlr<($=$Yq?D_~{Q;u2vR&FGy7l??s$b@ECeDPrMwH~M*p`SX z>GL(&7gzH(7?}lo%18`$8cCl?Nc~Q6Plyz2>-i;1teu>=q5%bMc06^qPMT?6!#~*h zLGc~-Op0>)%Wyiz^>TGvPOf-{$-qumcv;Kg6%1heJq;dPv)FxL6$~_Lo+P^K%j(aY zmCXFo7XQffDja_l_E>^CRPZ>HA%0uWFN-$J`&VEqNpBN%8@z3H8)a_R)SuNG=VixK zdo*>@RJ}H5Ug$v1WDNAp_&(noXVG*&l^Rhe4?DcxLW-=iAn=^jdul%;Biqlro`bpy zG|MmJ+&lVMnY-`6$uIrud4bL=(YrEKWDIyAt_LjNOVF??YZEHWMKrjeOdL?Z@o2M86if%{SQC6tRw2ULlz+Ww=-z@|V* zIgag+&2RJbZCbiux9O^@F&Omt>8Rj=dZ^|hxut+u_;Pg3|DIP0X_K708DXz>1e-O(Hmb;a%W9T?0S zRYJe+#8O^C5Na2mj5|!*ow^G#oRkru7?1Q03Gk7)>h5YCJW=y;%crPyn}jEY z{=p`NP7H)GltqSdmLbU-XK!6^YYb{>c^v-vg7_>N(o~aDW8FJ7Kxo9Py7g>>r*zA! zvK-NGdxwAAjSATp=|)_IY!Qy_ay9vo0%o!{M=!XaG>Dqh%-9KP-%}`9EDl11$dkO- z2RfrD(dfS^Q_Z)L%N{aNY4w4utEcDU_3vd`{d7xgr&`Es?y}D6{nP)P+j_-48{WIJ zk%8(Y)^kp%85{b_xRT3qzPD!pQl!P;FI z}J{f4b!3H*Fu|=fv8E% zX*BcjL)k06zQ+^xncQ5%*#g}YKNw6(SuG95u_LvD7REF?c)hgNv9Ky;PJJrGbidD@QB>QcQPC}+o0Yz zQm7od)-rpn?Rf4e2%ujL;$6>cQZ7iR8#&{28HHHd2Ok+-2!ETk5x1dUyUQw{V%~~@ zs!^P*d(POC9K>Ev`SXph3)W6+?=qPk|EUM3(lm>*Wn)cBe3lW^9N;aM5*W7q)d5yA z_qmebw`rP#jL@TsQ}mw;f)XI>jsf7;$Y^?I!;jd>F#FgB&N`(2k3_yN)OUCAk=me* z>9W)zS3%J>7XrP)1V51l#@ZIbj&Bv$;2TR@qGj5kvMcbjk8XrR2Sn3e$p&BU;i_)Y zDM*r#Fpu;8ZR~3P_cmgyJL46N*|&Y$L~f-gCYLq+d$7eC*NFf2s$2WDQ=)@m>t*M~ zjtbIwBaxkU=%mf?d)u5Y9~nIpdl?bg(K$43MRV7KVb_mw?JOaf8EJFs()$9?r@H_H zWibgPB`DLKZ@TCj)5-HL{Alfn0cRo)8 z*QE2lnO|g5a^CG;Xz@9B1Ow0_o`kPN>C1w&2mJRIP#gu^L_0BTRO`=Y2n6EdXA>nU z8Jd(270>K}&5knZ-d1mCEf{B#`);%g4tm)_AlJOtOv+nZANy|aK7-y?{7pvG`)97{ z=t%aw2 zm*E6${?TD2b!Z;2y@ATo2uq7cu4esNs;QP^#Hq400gjm%DNCVVUtD%(Kk#1e+feB< zA?T(u-N}DjpTQt1*5Azg8UrqJSJl1vSm+pgD0>&1Zh4pSpsS1mM1+G*nB)QH=Bg8O z*Yh0(O)#V9#Xr0B@a5Ege}r&3%hVDhXw0PHxoa%#H{yweZ7Z0@vRvK)MF6a8sg&Fyv#yy- zo5|hkXP=Gcg%?;%_j&6h(W#iNb_^L${g=ov4vafF`!!RncqA7CsTmGhw9M}7M}w98 zJd|Z=9Moyn6`Jnl&=hGu2fQvf(IVYS>|Yih^9es#h@C1I)-(8L1)`5T*-hhcw~wWr zURhAA7#dXDXnKS2B<)wuq5d|j;&4f!?wVDXyda{%Jw8pJGD6=Ala!?wX&w zriVJE#c-HIP6Il1*&G3e9wAv)I}OJB)|FqSm1-gj z3ux_=$B*b_I@s`E0qF16O0j>q&ZtA}89$J-*l?z$nI9Y<(tJcQ)`?w|d$wSRmf!sK zSHhrneZ&lnf`yJ6XH>aL=BR|z(90l8S{k$j3wa=zT$Pv%V91g#v672MY8#(36*voj zE4)>t>u-bKI^FNp`LKVUL=n)ivj>NlVup25;^J$CQ?)?GqJ;4yyAHc@{ghhm+&T1B zcT>*vT_A4`ea8+3?Desmcj)Bb4~N{ra>Lac(r#L&Lho930%`bwlp# z9@Un9##*=f__KAp@(O$1&qkJTpARBuBP0q_Y+~J*^{Rg2>DAPSoHIx2yUS-0cLmVF z(gn%P-0EYo)N%8bYSXgmt}t^)C|i&Hoq%I3O)cD|0EpHoub$5L9t7XnTK92Q>tlu3 z5>Dj|y-pZ>CW_701m-huTCYsXHFMle#GfQZ|0?4WCC(CQkaMAFRR&wz5scR5eE(z* z{?|0J!dJa6tA=DroE}xPZrz!#)!&?xS4usc_O-a-lTj$YFg{2e%T3PsbiCVU(*_*9 zy{Kt&H=?wE(!e#w_<)W*b@H35S_Gu+Gp)o2OKN~*lvN_NvNkdmC=h8wWmr8A&A;rM ze?eNX+Vs_&S%=}D%R(eR_kQ(8f_2ww!>iRjCPm86w|ZG+q**m6ExUf?WXoQ7(XDo3 zZVsf`oByE8sOT#1Drb@vyGTpUh#;yA(Vgg%Y2vDGT=dmbS=0qrZg|*|G*>hxF4!wp zx!&7B#}_5HGX>CiVkS*PIrmq(tIB1FoL*OLA$EQS(NmSb$jq{PK{7&&%{h&<>l>AF z)9lJaw}Hi7_5{l$!BxkBRJaXfn#S3dCQw=X5}@;!5LLIfrWn^FBRaoR9A`S8c`4#x z?@=El&Tx#x*;Vf!e{ecH4z;k1-H@>_SemIVPVeDBSKa(xUc&I%M|rGOuXQb0HPmw7 zKyG49avb#@-tayXa9TTYc?K;``NtLj=t4v-r*yB@}Y0!3g z-7jHjWMS{r@p5~#>oD(;kE2AkYH1h0j3O?*jea|L3@;V_M&#V!^(BmF))^bESxGJk ztT<@y=t!Z_m%|M?*Cw1w;XBlI5(%)|Hwd6uF79g#DylpsundL&%t@oWSc8535`yMc zW+@ZWTekX?uCrj*I_Jf3+RJG6imw!}Zwzo|6`? z2$QAL*3qYKJaU-tFYPf>zA&}Nj}@w~x|qX!wFI#9C`l$Ims?Q*-u~2sO2W%XXe?RW zn}4fWhIXi<;j1rhqqPoi%HV`u8^>G48=mUZLmyAmLy>L%l>W4&b%AG6!8DW`!ur>_ zK4My z{w*#0LHDdgrl;D_f@Qjrwd?z(FtnTgY*i7q~4wHL&F`Y>o{f zB5vhInIM^x`Ma?YG_V~Vdn4ekyiG*=6$LjL>EeZ$F!E~lyoLGLmb{x-ilpoQU2j-r-GL1_#$jBU0z~pg$vtmQIqL-WsphsT9f})Yom|E0 zufNCWbrhT97U4hVHjCagOKh7+B95`wT#`kk#4j$ZH6yF_Y$k}NJRnNq2eL>boHAdj zJTA1s%}}w_=5@TS4Mp%kPVw7Bl+WXU@w?o1$VL_`xcaSTkkr5a%w-Ut*fPT69{Tf4 zrbpp@P^TDQco`!T=~RoU{C2s;*DIQrd;D-27bL?QVYy=9q?4#Y`gn)3RVSS_a zk}s4mH-MM9eY$6IQ|wK*z`8ejzLHJE)U@4dxD>-u=3(JIXPiRjziQnV(mcBB$CU&fpLHt|z?XvdSD_rmDv4NbJ-uNdj9hb^r z=OJe#`i9zb)*kWexyA)sRdJ77R&w3+)D&B*#B%Q>S*!&v%G&(;^rd5yO1(=|H|v+z z>i&oQW*3LPnvHoe1lWgv!LBA*&E(tBPj}ATp8#9zhH9oxdQo;+e=}vNtgSJ392EjX z3~k7;GVt^{3mw_kk!F7gyOqvL>o?M5GD@S|#`?MF=+=0D+qO}f73}Qj3pl{LN%GCGo`T5PeG4y^i z?q7;zQ!Ha`rH&O$?Dx(@U*Fy>Ba$jC)IaV1^dq_Cu`}0r>)DN2F}&_YC&!p8Zmtog zGc@<{dWw`?9nFp^N~#WH8H01Cgx-j77W)b`>1Jx15$`V7=O>JO?ZhVE26d_>9=1kZ zypn^8>hUJdAUv$=Jan78;WueE8Ug z8b3k8J^Qg1{a}=}gWJ0ew|gQ6y9&bQ9M?@X3U#9sbvR6tI=11-MfrfEUI^0|ayBAU z2faxI%&&6&tBqSUAEcHh+Y3-94m!io(aDz=%czOk~`i-OsnX)0b(A76Utyp7syBJ8>d{bPHtxD38IXtq;~LBHbr$mEE< zY>U%!&7L0^@$OL!|Kj;gdzZcYW0Ae=8NWG~(ZQ3qfBHu0djuN^nJSFtbGrniT6#~1 zB@;gdr@!@RapO7yd}fX__OaSX(4%mg-W#_l6XM^lJUoT9+|<(UNPaA!%OhXD!;B4^ z@Z*q%ScuUa0}o5Zb$`e(|675mcyJU@^%zFdI+<-PP^w92T;R67XbUE!#iM?EjjJhp zk;RoUD%%J%j(zOdu{Ep%jJ?kftd~2PF)ZtM32$soBj7Q(RQx=OH%hrQGln2I%(dlf zbIf$`1MM$oik`}LHg3~5xzr5@^1$WxCBf0R&WTi)YE+wM8hPZG<$u`Dpak?RFx+H_ ztwjVcx)VL^en7@lgsorYn-c&;H8=R;09^s_hP*mNP<{{6L&eMvR@?KctRVBan*!8UhC z9rsm(_oUDJD5THUsqS50dl8Kw4;A{!KAZd+7}XqUMsUo|>*`z})4y=w;`udI>tBI( zDnp=Kq*j@xCPK}Hb;A(aq`5H8!w+#L%(X~MH>=}(YAqo$UoHDl@Ls8ZeUi7J9+Uf$ z-5l@p^z4d&O+#22-#rcgGIoEo@}}KMCph3b^};`c*X^SeS^msfNSS&{+!)x2iEQB3{S4r%Jmpsbn}XuIjCB)fa-B7X}}3SZbfbH zH&AB?xWpY!HN%bS$<@`rS(JIJ?L74(mDUkQ*PoFZi*1=(x^jDx zY?t{uCClNa=f{>orgu3Inp2H^HolYHkl^IZ+R;fjqtDmtuBfY~^KGL*DA)NU_=bK_ z{?l3Chu!4bAzk$x+k!Iq4^35GX@9D!z0ZHNi|CDM+p}X|g^vIM80F?`NHUgY9&zZjtV+>N3Pco!P`$R*YqI+V-h*dh>&|AeoJ_y5@)em z`1{F@6SYzi?S3HUayB$CKo9J;X$W@==E1iUMC&g9^|jVc(nJsayY==6c{GumuC|># zG|he_t=;-KumdbLKeB|aC%vt?O0Y5=^EL~=Ip@${x+ifFZh|U~*Ab?WxL zYP;%nn~-obtO*;x96#ENs{5Y!XY^wwNMuyzXFj7;cB6NwoH`EbZ|wGHM(Vj#&u zMv2lM)7<4%)9{db* zePul2r5ymW-nkK0-D~R@+lIUq{`=N=Q+tLqcsBcXjy5li@*-2g`$pkD{+fEB!%X=% z7Rs!QC4c=!i%F!f_Zw`9z!m~#)&+Rzh~SUzFZVN(WX47T7$k-5heT)LMvXC5drE9n z-O6qa*4d=kh0yU4>CtsZiD-;dwaAhQmn$UBCVz5EA=<@P`9p1(X=j=`Zp~r~4LV=_ z|L+CxQb1TR2^w_r?p~laxcu|lj`!mmG#IrO%T=b$)VhsPI&gPg&$JP#c8~U>Psn*> znM=KG8W^9JII6e!V7lT$~~P_O6$dn`5j4j#rP&hv~m> zy=&5p++Gb1`wDHj5|kI=3eqHb{Q25Jy)(!J;&i7uI~$BDe{Q>P*}cSpvjYx(2%XL= zDf;=!CqlSrYx5CmGLh(E@ih=YO1)D2z>v?&LM8KeKfgboX(<=+Zx8YRwJKcLW+y64 zS6db;Ou59`7O2}LtRnDoo(fR4r|p8zU(lUeeMaq1#_Oqu40PiIPPJo%f@)ToERq}1 zRxZ9SDV(;JVG7cQJj5kn`k|(!5iZb}&e5zR%(dD2F5J){a&SpBQGD_8dZvVMw<{VG z>6)ChrBX#}Etk-I3lSma`3iTHhg6BTAip-*|eFZ{+W z@Rp|Maa(S5I;Zz;l*9Al&-n%T+$VgXM53x2>UGv}UDCge%$8{;^t?E6*|9GeFGcE^ z65jQOdF};ie>e;NQ0M>V=XWvm!QTLfawHFG)y4;G z`5SK8+^9YhniNiHCyZ@?DQvPtS0YyB$u6-)Hqxwj|y z^$j1R%f<`Ly99-#RF*vmwCOsKDQ_0L7j-q)8T|BA{R1{x9Q<>C=w_z@(|YI>MV77_ z`LsNVZG&4hk(_ahJ7xbz52-qqS&-s2Y@JFUjx0x~HdgW`@Pki6bH^iu$LZ#$dxONq z+;i7Rx#6v>q+85IFMd~qlJAw;%bNFEuvKPC6!qHun(&ru^(z4|yvFQz8BOr%)5f23 zp&_t)7cjy6?>hJ1$M2fkFPI3_5oldGD0k*V_rD(}d%_nZ@&_2jeD7S&XwXwQP+$DX$fLKpo`M?CxxK7_M03jW^OF<_jk|INQT~9<;?+f zshTk2aC*zKd);Cj@SIk-3sf5ogIxVP!g@wJ#(=a$&1zlLg&!?-xFI4*)jm%+woY4i za%cd%PCJi%0qoE~-*;@z=f#w?ME4g3dY5G2RA32vvtt$nc;p`$@&d2d!lDVBm z8Sa}`p!$S`E@OLK^^WfXTQwFKEe)eun&=`Mqdv|wT^WpS2tMNrdN1kQ7rAgt9LhgG zOXLDmS?%lBBtlP9@mG;bJ{>CWL_LPE96}xwY8^5Ccs|=N53I1;H;vNHUtcJ_-Qcc4 zo0^^0yH_WM)yjYdZ()p&F`?NF#iN{870(|PM7y&q`rD!x#Yv~-(irWq524=We`%)a zt);)w1mAS+_0?wYzuVbcu2@RaQU1QJDP{1mI`N&cLSqFQ|IY`tIjs)p6(KQNIhI-(B872+g+&oDrUe8|AEZN2H$0}&BTmT-TlOC4E1~9wq*`u zQ7Dm%mVqatEi6nt6FY z!@Ac#aN2BUk1S38v4zx(<7ycW1N~UQtaZS*84jlI7Zj((ujxj9yzTcVn@!91rhdk` zCA((8di4*4yq-*$93TMKJ-8ZO^xt?#92qk66TR=dEriJ}CWa?+nUinuVBwQAK${Ra zUb1jLm?Xb5Xo?@bGZ+L01Q$7*qJ_QAy%$?eX%ihX#KJKy0Y8hW03jeC*n9(>|LZII zSqFV9ha|I5=LYe8J2*SiwbyXRRH-=z=Hd^}epl(ao;Qy_<~nzk z83!ZmnbG`ECzASNgBugVB^_c1Lv$*u@!0GsY$d%Oo2u+yKa%q|%*1vo9{;&r5jLxL zdRhTVMCMXORW<3-Lx3*=-WhG|jo-1$@mPKn>M3BmPy^Gw?(c?HQkLs=RAGKNH#>VV zY6P7&chXz#A@YX}41jLmKhrTX6#2H_6~HpLHRR{`eFPt>M!N zw~aj+o|IwX2rWNOnbuMqu22$W+t>o?b~HEIyMMiT>vfyQk`VgmACk4?MO1J`uhW%R zCcisVYHF&mkK$Ln+;1{ry4l!U(>fYiF!EXY?`p&O?Z^ z^p^vr7f>=o2IdoFS=d_^(+VAJxiVI9H6+-;)>xHzdg4@5jX$MezSYh5n8nrWlB>|` z%hvp8og?g0UpL=bv*m+EJ${a5j|T12)t>kJ^GbM(AB3T{BU8q})#ZwjjpcFA?|nLc z{Wu*SG-MG%eysmVw8Z@OvoVyA(h%v_I}!pJTo^13b#<4M6lzjA;o*j;|ZOsKw z3=_qUZyY)}frgeLUX7DVZlnI`tFV+Oc7R#Trv<`Bxf^%jCvg)`ied;UFv8X z7wc?CMy`Bt)^DrM-)-S~RI*86szr*0LQrzB@_QuvTl^hxe8=S;89q;hOnMzUfm$}n zFP0b6f5&Mku1FfFpEqlYE0Q?dcshb!*e*Ty#6o8ni^pwt54$1Q`Qm@dN9q$6-L;<|61W?Pi<#3u&s)aJ1RasO35K-RV}X_ zhwvmA^rU56BD9C*xkit%xt?NGTMEX!nv8mWq7?#p2OK`tZ7n6-&aP3^4qg!c0TV?q0oW~OY}vzz%wNv~SDo^4uT zlbk`2#?{cD;zp9i(D!N*6h*tDgU#2N{Wf3-LHET~>WW|a-p?D$+f zJ>jL|KSv(;WEM*X`((20Q6o9Wf)!Th$cTSUTbBEpXZU`u_jpb!@px0ysfDXx$f`mr9=xV8O-St!j6|1c zMkVVn1c&2KorcVjk&#?v*gZg`KUitSk&4G_-wG~t2A5Z$_fLx~=f2s?yJA%}sh`HM zV_h#4!xq={^On>J&GE!(^BB;gGIoKpPjikUc~^f};OH+c327&@%-vM1Hooljz!)0U zybE}#zOq^`2)3#tWfU3#Vv~%UB8EaX2RS($o~Q?prv5+R&eF276m)c%00hBdH~_{& zkc6I|;lCm55^Cz5y}cxGGBK<V4Mi|0CCG|wT<(Zh=b*0kJO;sg)&wuF78g#klA`}=ev{T z?)2YB^iIys!8$ukFX?L4iqx***-O9G7r3U*mKWWdR|ZS7xc69PqV4rQUFAHomKi2H zYF)wZ_FGS+FWMob!fLp%c93zuLBmNedV)VV?WSs7PW>eXJhuG$RGB+SGOi^}sMDME zFi{53O#sQ}c)oHs3Fei6-|eed7@GM;56sEeFW+Qi`=l_O*O{Th@0kuK#+ujU4Gr=4 zI$zkx78)EW4chluo%)fO+HJ{D@%~A{{*6z+TriFQ@A`gs^;0hAYq13E=9m}Hyuj#I zr}w?dbg|6M>2mmHU$xnPH&||F?mY@Y&{0)9oj?i@Uqxa;@+#<}+)MpYOA> zhR*KKjZ+M1H(Tr~yiA9hzv{y+(}N|x-bY4)rXdWI!=LVPEN)-6mf<%Pb@R0C^@Ei0 z*xP?nm{N~ZM*laaEuSfnDUyzEGK*g1alYzK$H3Sp4F)YWWP&h=_xe5vZzZSZ)ok1X zhY=Gx??Xe6t4RmK(20g7CTJN2iGRUM%gg_Gs-H+o%N+rX;j!&I{^*)t_Foe3m`c5- zi#e@j7#@cWvUXI|*1M(6fFID8$Ik)}=*!(B7E(}*wYQD;hFSdSoHo$d3lC`pg%SX7 z;^TaQ32$^>*z)k{?FnK*gWn9SF#EPm1QJi+AoEy42`{jV(DRAWJ0S2Uib+TmmzFM6 zU=eB2m=qNj2S3xL#AMd+I?S^KJNyIHUR&3Gj4VF^(G|W2zehIaVQ-|b)oA&qLF&^5 z?Oz@K6rpH?@1SuMs)u7|jJ%Fo!fqP#%*C{>MoGE zIlu%<&&4DfRNvNECK)k=NyLB{R89(W&Kt<$8a@qBjKD z=jqTwnuC61ba-rRcW8=|1B8__o7ug=DH-=kCVE957Dl-H`4sdx!0h*DiCG8{xeJc; z_Gr#@G_A71b`d*(WYs%i<2o@h@y}_Il$u|g{(c&O0UV}`moK!c--{vU$BTzVzY|#7 zxE|V$b#aUuA(m;>pea;-_}zf9ue2yK(YlzsS@q^HAoaabetu1Ic3uIu!RbJ zUh}4-V-%5uK7M4SomGd z()m{4(8%XY*voa|=3qq~o6mAer%A8*T%YT^uOiJ?>W$9Z=@GDe52*7_h%x@7hwt|F zv-j8UWkHgSHVg*A@5e(m6-}K<>Xo$%YgFos#+ z_xePB_?xG;+7{SYLD!^hA*9v`*;bo-csny_*J-jPh$ZCQ3R5Y1CoLyuHeZps;(hL) z?S^tBgN%aeo}4@rQBmRba-@~~iS2<_pV#FJ;FV)YxpT?I!W^#pk>ByT9rS2u8O5d$ zeqv9iLi_AOFq+;y@sD7p^>$uwvHR`aJ21cdURt(&(=U$m#d@h>C_%brw_p0jigSI- zj~;6IPr`Bc!VeKKF=)QCe%(eo+X3(KX-pl)Rw@lS3z$-?N?)%fOVFJS@tHKU_#`M- zO5@p;$($R6(5F=-i92ezvCbyVK1vGY5rugOkQ~id$~ii+T3K1`4?I41O)x_^#zfLr zWQb*Xm{_ilu^uzxg`r3R8s(aa87XksQSrn@?$yzcCa1d7GeJt()u>&i6484nD=Ixk zZ%^E%ir^qAq_o#e}Hytgsup)|z82790r2ame@fpOyf>aSRI_s&bs>-TNku!C=khSzE zdBhO&HBd4#g0t;AB1`1b+1}=~JU7xmY+WN53^Bi5Q@b9`{yDx4E3P?(i1W3iPgSs5 zC8{*|&7*$`-QGWuHLqngN&}_C76c4w6RXg=t0re5_vct9vsmij8csl@OD6V<>gujK z92h37q9iIG>pzv%uWA5ek$?#TZ#c3utoGBWyGyZ}ikg=7=t)K3v3;HOG}Gc>xtSD5 zrl#)mxa4uCs|z|c$#2|l70{==w<~<$om(H%vj0PgX&HEyK-U2H(_nxcvxKaS=cCmt9dqFC z-?G1Leq)g{ly`J|4glB7>-&c5MhqTu;I84%UW|$X+D%xtH#&{=^CTL8Io`C^y738%1MAeWo=9qxe&D)6YGxL~b;qR*dmg`lm zCCuTz_`Hk1AQK=dp8X=E8w1&=W@pE#R=kkQc5!X^h{|olJzw{_Q1ROm+!h)v_8WGM z-Ns^TPZtn)X*V}60Py6`(OfyN%K_$CHn+uOzR1nV;tY*OIw4dfcGRCzT>hSUg7kp<9TNl9Q^_g$Id0p>%8kXSwHhs8y71ARKH z-+?UnHp?Yt8oXv~DE%$FrHl8;Y0fHvS$a9Mr?!#SU|xfwA#@~Y@)D?-zDTj+vIbAE(`}7c>Yh9FK?Q0+_SkzS6@;pKMH*|B+^cW zU*Qgqa1bE3lv`*)yTOG8}{DGUs z=5j0@%Jz5ss{~_eMY;nVbJ?r9)u(>UkKeG6BE|}FnC>W9^=x}LZ+m+M|K$p1OOg7f zU{OjYWMo8O24!Ut&U_a3d*J~%z}E5BidD#X{Bg!V10$ZnQMbQ*o3N1X%5eoKwEK+pL} z@?P;SA$fPtGY7hwJ*h*BF5NrL3VL@dkbMyqN+H zxKRLBV2#tR?A|z7PDTcqj*f1{X_PUBL@;ZnOsW3+AJ{ZTJxK)xbVj|VAAnwLuwPx0 z9PJl5(N6^7U8oLc0)y}-T~)Q|$k{Mx&dG~4V-!F?gZr&M1eu^q>s^ujOMe$GEqves<8 zeSUR0o{ycKRoDB=6aerTmzwNGSKZfRORYK{wxT;OdQk;#XVn0pFu?Q$R`h#zi}4-6 z(XTlEB}OCUh+wE{37Qakj`?qtOsC#_s>|0qDmofVbmRUxQ`ld~$mbHi`|b5yMo!N3 zaJnRhR5%x4B`6HDzhyK#lYnDNmCmH+@*3H=Hs6_>1GW{A+xn|q=Zlm~8vnqlsmVg9 zTvC9bg2tBtcXjyN#9Eo)NK4$R#HfS-76!nx_{DqX%>&1&hxvLu9?|xYrCiBo-nZB6U$kHe*9>MvC!VL~c*)73Q66_nbdc zRJh`{`iVN8ZPLTG6PEL)7&rVb4)X6+?XnBywsH*Apd;QKJHE`98Kb(y;pYwuHj6|} zm>W!N4AymW!Y1Of&9A74X>&b3g7BmLAoYJ?E!V8visfF*2dtyQpgj#hsby7Al`qql z%vS!ox^i01K`Z0zxS5vw?>$_?vPu*4^4`Gh$@@4FU}rOkw#HXm318WcsDe)mlmNS! zw3^um46Ltd%^sVNBo%e-F-5-d5{ybor5D<(N^uEi7=P;Mg8wa$1 z&~|eshz((sA;Y%SWB{efb}9+8xUxR^X+yDkzT~qU_A15Aw`HW{gu;J!=RuWvYzRQcqLbp(_O!%~X z<#x1^`JW?zbVF0?>exas|AQS;%cUu+sfhvRWYFePpi(3WFhnkZsDXn77lhJY;7L4w z&K{J78Kej5Of?%#a|GB00wf=RFAe0*D%FO9S65da$Cb@42|yIxT&Oj*%qrD_|IFji z^CR(9d+zJF)nejEr%T6o`^zWoNL(2HD|d(MG481vV=>(Z%Rd0VTD!p#>-Ok&b10GW ztKkcRdn|z%n2!KY!n?knT0i%Ck5y7gUq@s3+TSO46Jh`$U4bW_1aiNPXI+zt`(2w$EY|tRVkb*S~5-_`K6`hSyV22`wd!S$K)6Moi~UK zeQ$ATlrl|{W$N};v$sbQ%Lyl8|8ps5q<#ip$Umt`(Rwe1FY2sxdgGJ&U69|eu;W!v z5UDAXc_s3pcynC2)t4#|2>S^DIfTjXNGgC&a{}b1Zjk#lIsK9pE9>7u&o#Bv` zlcQr~q!t#=8;m7thaCXBuMGo`+t%xGeyf>M^vime&+p#h-~V?;0OAJ{DoV=WbVIL5 z0CaXWDfZ#=aX)Y7rwXOp>CymzGX1X_FqXwx1n6ZT6H5cURVV48`XuI}Pe94O6vt@DVq3Rq- ze12hFQh@L`U0wPtj{de{aSymGqoTIbET9awVe=&W(NeXju8>Xj$~1o?TunyD zni$d#*N|}hHH20jOGQ!ENp{E!$p9`MD1}7*rdLf?bIqUbT}>75SD6#Nm!Mc|b0x~= z0@36@URsg>H|ud&D`g3W`mzmLyWsJi67o^tUI0^qbvT$5F?`vMKA~IbQ&SR({BkOsRM}8P22ee%mxDw=6tp->8Ac-!NMS%QD!>_; zDH9%*sASsO%La*{!c9_DXx9eG#LJ`TQiR#yeO;f9NKi<+iW=E)3`aAb35%^TXpSRb zFP$lrU;;6EN6(dOs;KD505YT1corRaD^-1;l()w`E$wc5;xRpr{olrYLw1Ry<<*@i zJt=f*Ka+FETO7A}Jg+_i9?a}|PeFh7H*Q~5TNIGl7Fs3Em21@kDg}C-XXkRZk_qVL zsH)XAn2Ty9;qA&2Il2{tAzU#8=ViO*#GIUeNz`wnYl7aM51Zl0mV~&txbm57nZR@} zqJPgsVJPaNK}$3(iGL1WYwLeP2k`#zI7+v7h)E$K;{Vlsl9H0F6FH81T0{420ab3b z*1>C7tyNus*6voR+c;em{|?OOI=$}=a3~e7`MJ3vH~!Yv^gv==Dwod0o|W8Z#5$;?S@kd0~VgEVyc{#3`bcb`tCW>IbfxZt) zJAV*KixrNW$fynvU0Ysg)2}t4TK{ck7ejspixW4lu&+o4uhOFINIjx93v!qiG#?OE&+N z{2=&DRGieIhcgy6l)`z3?B+E-k51BJwL97He9yw9($Sxr6Mo?L)MC<(OMT_V1C@h%FdG3{O&3IB;S zqy_e%+lPn8%^zGLju+X%Kv37z;zY9~{BM1v>DiMvOUQZM9|o7gfAU*JI+~TCWhGC{ z(}&`84!Z5ff#tz`p%S1D5n%5x%HXHjM|GZYt#T6y9`KfYcIfQhrXN#9iNgg2hKUN3 z%R}kk9~kYCgxU|O(r=yztL@w%CWDb2e4}FjH>d!I{pSxW{=x(Vu7^MgOHvY{)9aRo zfdRF+*nU+Wk4d-xa+(dut_j%p_WwQ3d4-oT!lOd;?EOoVcx;rgDzyH6Et}?Ev z?~Nl48KNlNAl)I17%51{KtNJ*)aVifk&qCi8$>CAF}n9d1*Bzkry|`TrKJ3i|5tl) zxBEG}_nv#c@tp4io*d8b&pW~xOi{Ec%r9fXE6a0rnD23Wy!tvt78_h>n*Jxa~Ts?xy6k8g-XVV&we9VcS|zG zk!A%Jf+Soi?+@eSTcC_aNWSUPtEi9Le45bv@gs7s#Vr#_^g?Gz2uQRi8#4b2$YUWzek2oP(1cPgybJKn>LUC}4FxAJGI`o)0_Sd`JN zW925IKr3P;9nei7Wr6HU`{WfAFbL$;fG?~KQ$;ipVFl36=EAGL_m2bJpi9d!M#X!7xX+S~?= z<#=yW>62U8CnvV~rdp5uqSWNnRD;TYt2Xa(=rK8rt)Dzqu#>u`<8q z-yE;sZ>a00$az(7zcPA|M{o_B45wFn*c>XUXE_ffN_KX3k;~b|%S|2c>EF4o{I8C* zZ;^b_8akP6b(jA)ak^w*(v=9`P}^&da2$@d4!1m zUV)@kN*4A}n+8oQ@Wuu+B!?r|Qt|5iL~E?$-3{*QNtD{$%Sl4?Q?J=Lad>VKMwaOw z&~E`akg2R;=Bshn#)FBC-nEO_%J(HDed7^lWmU^_Uzs+Fr?L5FXN zEQN)|z?BJ$K`8M@mA=~=E}$&GS|Wa6?8aJD`0@|ac$rZG$ZO063GZY4g;2impGQJ+ z>+60C+K6S9^flVb`ufGduxl~rNs5H~zyLr4rIYH$<@Q1wLR~vse0c?<59FLk5=Im&H+rt&UuTtT8BZt zZD^mVqbm4_Ua&=STvS1+Ts~Ao&MXaTgQ$Qyx38|OB&MZB46WB~?!38f(WL_w*9!Ox zfBZuYK53q7Q{(6NdQ2*IRzx*e7u-P<-;+BHdBb(}_GM6z#G3?$#S;SJ7a+Vt9f2r7 zEf+KJ3EFq}XVtr8^$oK;M$I)c5)epd!M!=5y?{lJ;EV0d=&OYVoCv7o4S#3Po$3Qf zqv?Fh+Q7hY1jI0lyUtQ1xe$HWKX%Aj{2F?dnk%^8w4Q?;%>Aq6>njcg=x+@zf5ihp zy*&<@@|g5n-tY>24AlWZ=uLON=ST=q&{|~r#NC$-*@R=Ezk{CNW7iLPw&;42X8#INh?9O(-B+}fDjQAdlt1K$At9M^u(sr`n)|`%>@$gT8 zM4Va)JO4czn)(HjBfXxK86LO%M-A+ikv6aOLB_ztUS5E9H^$HsU@&J(F%2%pt?}SG z4_8^rPgX62q|yNrY&%vA_KD7f1(=vP>eagKN{NVwY)seOBs=|-7Xp?jU6ZHE+lCc9 zRorN23!|;6sv%;bmanT}f)3m10_^M@!}+gN4?nGCeO(*VF%oqe=WcYJQ$Zlu@Nn@N zHJ%+RK)q8|QDIEc_Pl&S3k+4ZDrY0F+B$9OyicDZpO(qxd!q+os9B|8^OVHv3EzLe z1~P`fzyHeW>f*tRY2cP*OSl(Tdq{vuyqc_eOBd@_P1*|{bCZTGaypocTJDcm!-d_G z17_2p<(9T_Pl%)+cwrX(V(?yd3`N!uvT# z-+!x`7{%YA+pC|EmNrspe!r8oZpb2Xhnt>-b<+6Ml10YeZ!%HDtx`EvDuH6fvc2a% zOOS@^HB{LOpGOus-@0+)hA~ooUXG##rq}PBnp0 z93}o9UFF`n{Z=#37Nq_5hQF&P^nCQ;5n}f{420BpI}!G<&Xmcwxg=;>2Mt6S;gj#% zrYGHgol;Ym+;DEJfy&Pc4}Fx*;-~uIR>96(iN@C51DJyF9fxJr7Mq@UMxUMGP@%ug zk$}-fR)(2nC@ucfDE(2xXRqGpZ`x)o8aoB;d^dhv!WmH99N9GTUg29$!}Je1IYt9J z$apLsmuv0UpnC)W|JY_KwRFa&C2I0)23AY@Z0M6;B zU=r=XgpU{b(@Qso`5;JVi$2Y))Wg?RCWm1RRL^Yh($HCjll2$2ty7G?Hmi)!*pgkQ zNEo#k)SGtUjHc|&36K3m>x0{Hk&L8YEjQe%b>2`NQhfM|@{^x4K7XAUl{!~ax`-<} zy`gq1hA zzZ;M9nEeHo2?%Vu(J5moxDAc1E6_HgxZ54!3qev>eB0K`G%AH=Ik5yIZm(l_BVeIv zKB=7#HHFMFO2`z$;T)YC$;yU>4eSakH$=9b6;XebJS@1|y0m94r6j9xb;*Mcu0;bA z3yhvjMae=PqZ^RbhQvzW)a;^F63{eD?EL+mp$XxneU+^Ds4LsRiyN;q^;5ggeMC(i zm=fLq*n-1pXq(3uR(p$%;!_l(st%PS^vN~zjlD`?YMYMv++Bo4p?&(FKG+*fc9EHf zYS?jdtt2eH)^_?0veQS%)&me;ec zy}iAaPgK*2?2qL@@kM(yTwRYIlneDdesKR0Q~{9FqM{4b= z;;67i{g)^%Dw^{4?QNivhb`<+r64;N{ba}8xl6!@8SgS1fe zN)US$#7$a#gmNiGmP!h;CDNvt#06G759&@a6SBHb48aeO$S~&2^u&L2LqNtqwxYKD zf#Xg%1F@rtz@s!+;@hdH6UmX54x-MdI?u5Sr34?PdYCV!->+qhjXDwtJ9D;BZWJ^n zcAwELjGualhcP)Yc7CxC;^5$DD_G+Wu?rNsW8D3E)+0gQxvkMJmaW0+a)(~L-_y&q ztt%75oTJ41Kt%3)>~7WkSKKa&nfTB1;>APJ#pprXR(I(}Z_oIJscD0dCEGZ-iMgBd z59+0xE6-dkgU;;KhV}?|5PP!?F#v@Q1NNpWGC43HEVoad_?BeV5VHu=mlQCky%epDyDu{ksciSzuo1v z!A>s(Vo$%^P&?3Nq=#x(6v3g4c;12t4g1f`e-FFQ(X}-9BTzy))KO>E{i4%n7A?L& z-v^&CG!T+&6lF%6*+q}NJeaR$rj0Vm?MLjX@XPdJ!SlhH6cE(*YWQ1tf1YVY&5}iK@zsc_*Us}QsTdy!x0wKvHPrL;H4XB>*=(U zysFLW?0k8HWYRSoLCvg|V3{3N1w+?=m~n}U{W5-eEY&j=KO9OP{WF3bz0!Rb^E)P? z;x{rTqBtt;U6h%X=Y4$;Rzv`kI24~vF?h;K=3H1RE;yRCLml_$ZNlzO+(3W~c3J9% zbzn?brk9sWj7UzLMNq@){9vcpS)*V#_DOV9!ft;z74g^7*MtYV7YjkU6)|zq6$=yW z(OtEtjqDdQgGg|u`Pcy`41AzhGNenP|M}r~_v)!!pX{K0!E#pO@IlS1&;D!nt|t1T zOL>%pHT!p_4W232GZZ@7v5mBEBj9Q^hf=&2oN|rKd(%sE_kxc;=x}X!hW;gJY5b1_7L6JQ>uj}GltG3v%nbl0|6471A&$AYIZT-7)k4qaxiU&CuQ52nY-N|HBjR7DYDP3{2q51iiXLT=n3>Hc@#>~kzM z10FtcmHFtZ;b`INVd7$bL(awA+yP?c=xU^OA9y9PU0zyJ)6;0b?VhKmT>T*ua@dL* zxjJY;EkKsih2!>}M{b*jcbZb(3nzX-beEFSH2Z#i=O^Bzn`8lxg#BMWQhR^<@%NYC z-rxHEi0+Z_y_@fz^!N6DAJd0U&sBLe6vs_+Uc-#XG^dPgwcP65%Q_Uw{z$pmOv*_< zuq(PAhb}#>_IM@02dUC64(OGU1da!?Rr?JFcA_;+l@ScQ3P1;dVR$Hg_h0$#-G1Vd zKjPoz+rMA`TdIEfe@a#vv$MhWbuAOYf4|f8R=C=Kk{ZZbS4sKro?YB1EPScuRHo`Y ztjpowlftvfa(Yf)^kLJRQ>(EA_@N>0YUv7oz$FUJsn{uI-TTj{A6R&Qj~KX3QC?XY zP03t1v6bw}!n>KlinEmO@3i$8!Fnc^M~ZtUhztvKDQ=yiK>|NE4c2hy1pyIc&%rAU zJ#}e$OG$}>XK$uvo73wp{!)KFABr5f5iXu#vv;CbNFtz7eRh`excBst2J0}*jT>Kr zu!g-1{=a_h2}A5ex~nv3DrD}uDI++-M_LpTyjViT!-$A&t8)!%U9Zu37p|6Fc86(q zIBx$jch1x#kV6dC8WkJ8o||@Yd1YHfB>7QJZF)hXmcNvsIB`X@SRDAkqbH#6O6r$I zuY+|qU@g+*pg59ek)qoz2-P>TBmBn*L-$t~HYugiI|Jx9)T~oKA4A-^-S*?@_d-=3 zR4$oYK|bEI$~_m}B(Hl41;$bOeF)Jz^5@; z{ekf`sxpl>1 zFN103zgSR`BSN?y>KvR{kpeT%G%0oRoJZg<^it@$8Zb1KC}3CaG<(xX6)YRk0jH@+)at(Axj$&X6P_WWoX7$o2` z6M#`l>Y5u1tRmX#1V(>=R$+zaQ_kNtlhW$6z~tA@L7Q)+-IS#il<4N_i7W?9?DF5$ z+eGziLMFvtrT?g8RwOCN4tJz9?0H?oJfgm!l=Fh#jUX|UV)(TyQQn$^LTKKfoSB*O zx!1&HxC-GGB1;n69#0q92q)WrU1FJ(4pe<{R;Y9NP34l6*T;8_MaxcTlioIPf-f&M zzQ`3qH$0ZHe>Qe*6qhHS@8&uhzv@8IbH9kPQTr)XM&!4iUPiMbm3|XH?X$|=rz*%( zSLzX}KSbyHJPe*xAm9tZAw;Q_N5+<{3^^ukDvcZ8bl+|>m~M_`Yc%-fDY#m9bQNcr zzk+VMB;Fc!S0I_Ni*M#Ue^C3fT$IAdX5^3PunDnZn@*3NHhab*XHFwLYF4ImW9N}O zB`P}4TCK?ZRpt}D>-{I-W9^~-m3>e1fq$zcrwztQ+pI?*>G)+$ylNC87r8Gt)OcBU+rP8u~ zCaY+>_Ss^iM#YH|dwgvtnzV#R0Zy+D5C#VgH&nhj*IiCJ( z%$_H>al?s_?Ez_}TJqa&;m`-WXI{*qh!?*-?l%}zTHj~)t>%uY-)_UGgOQ*>*1hNI ztZV}D)rP;f$QWj+8GyIhMUBFUMgtb#Js5C1Xg}s6{|2Tt|L)>9^7-K-U}ROB)TDcV z`aM%IOM<<7^stC)auayz#_e?V|LUwSj&J{I;Tv?U|8L34(B18r`0sBwF7N)2g8Rn6 z+fFMKuK%B>H{U%5(em5%aQ?gdaw994h!hs^?BC5#K^v`#dojSs-uUv9;J;<&jsGcC zkxaov|9*A*<|qTB$ARG8obmE_19{3kT!qn-XRyj&@YKOJin&LH*XRkwqG8; z+js)HnNrVzQyHR`OsNEBO^itI?kUhNRmmf5vu*Kw#eu6Yj&e9Wn?yS(2+65CoODp)FTusBrMk`NOxZ=3foQ4eGtDW zL^IXJ4(izB{`G662g*D7(sY>TN2vNa?`+ZQAU&$5h{2}KQG7Z(_R&z51x^si>1`DY8 z`RgOD(WYMoRk7@`IbN4(zrq|2K(zslv0d{axKGCH}p@#!8=(IqEueCXnPioQ$P z6IbC{kGjOpvB3pvA{&LoO-XxPXiQz7bFjMec{UA&q1di?b3{?Oj^XAz>iWz)6PkCK zDAgYK;hoqjE1;?`U-1l5>o158T_R!&U4_#AY@PI~OvqHojpf^X+JBSrZONL~$@81o z<9Du?B0L1gTnaq7yqN-xY}M8Z=(Ko`Z^h{spng*lj@Rci@D@M$qQb*>Q|149t1T85 zK0w5zma9zKsPNBI`FTq^kTP0I@h0Kz8#gka((xMnrF}%_?|t{*qI$~~+|l*$zXE%s zn_=zwzbf9oHHmTlw|H*QWmY1R2>)F@Qa#G}uQ1=iHUbshxFLD-Uj_dubJ9Q(K9~FR z4NW%{2nO%Giwi|1gg}Qmj_&*=2@ZF;>rt(viP&iNJkBnplJF7Y=B~)heEViG1`daH zK4>QzAXrawT0x&94G38ib_u`yiO#$(c3f`1#-M0lfBN}@Q@`A>g&A%C)-0ay*|{>w z6F)AUYxFq;0%>~88;!`s#IXxws!@BRONkAR_^gHNdngu`ilP4gCYO!S*M!g~^J2eh z<=4wYrSb-NHs0B+BRTGgIdDw&C+4xedbQwzeP^+x>o{00wWVyR>LBafFjuH)eB+&X zIEc^UV4-QEKqV?Pl%Ys6Me1z+3xd^B3}#)QhG`%nw5dzi$=Pq+wcpgTm#I^r>-?C%f3Cf$J%)S^Ee>YdkmiM6ssX73X;7xS4R zqDxC(kyvinn3!BFEzV-x9cQb|<=mMQ459888+%j|HkaGmJl%4n`c=Fq5 zuGx?SO_R@+3*JjVhZ;JNKCay7pBOz9ANc zQ$JaPPo5;kqNCzl@rCNUV1tt-W5p$COaKB&LQvWQz6!JS~oA3Djk1qzA3 zwi@@p!zq_J+>H0ySFd!I%e~KcwGLuUy90^s=j-h!@|8|Ehbj4O&X%BuQ)T+7Ll_>q zKUZh$wUI!{a#AXnC#2$kF&+ns}$Zv3?R% z^A4)5JyO1R9Xf(of-6O}pv=wphv#I{G46*manCxeBsUNCh3~nzM6GYGZ|sRy9==`r zYN*@N>ic{n#9#Rf;=Z+yr!3_2=lrJEmLq9~E$&qTiP5f&&J0RVJs6|}Rl}L+DwZJi zPp{RhuUGxc$PXM_-bN{8g|<|0bX`+l;TK>Bdh^ajPdks>Ioz69hodO?y(6GTK2@P$ z2FAbtoyqMUCVL*M-$1pM7p7;#wA?oYk8Lyqr3B`_1vr* zVR*lTgfG&v152k5n{iVGg@I-_K?t6|$8 zXr&M2LRN#bU-d`M=UwVXHz`TX2O|zBL!WP6S08>jP8>tBej8=* zj};e8yTmLTqvXtPi!}yWzMLdz%O|)?^s(|&=jHL5al%-vYj^aS_ga1J9Mpths?W^l-Npy!rBM&ZbT*0CHfwS@0s>3~CIW{;pEiP+ zZC3uxxz*8&oxoTWUUU5DICfM*b(fe%O92^`;El9kBBoIrz`#E9oxhZNO0q7vPYxE8 zDA}FGEL&cWTj1NYa|H^6Ds+z_9I!fv1V39Nnz}lN$zIgF8-^7OhI7Z?4GDK_zY2Ww zD}h6Y%eehR6giiHkk`pewgg|yMY=E~&_!6w{cy<_Fb1`?wNgPOYEHXgpUb1K1i(Bm zVlLOBCdv>ir3S5@&V2{_FB#o8qFZOG7*j(}q~eAa=Nk^^n(eoq$I@vQ_>F^fg+pdL*3V9Vix}tQai(S0z(=5`Usu!m zb=^JJUbYQIVLy+}2&9sqoy(XF&H0>PfD869Y%Zf^8<+-*!>GAB*lP8)DDwf{+`yr~ z@tSEMM;ipoG((inhsYi7iBi6ZkxWnzYxwrX;1JNuTUVM zdmJuZW7r(;(+XKVvVTa>PJ3ow;kW30vGWJ|>6ClU4Oed5=ZJCJf8;Pu2#+7-MkuGTTIuI2zvx zkVT9M$D}-ub>?E~4{uIh3rDWj>roKKQDszEEA9FU?9t%|L+KY>oEgPKJ;ys^*%X=t zH-2(!kSxJtb>5lJl+O=F-BCBYjg_P9ynnC)dT{>sxB5^LkAHnkVPWC+_O?ve^D@Jh z9|CXDkzt%~VvcXNs0*|?30dl%D)w_ftAJk3V7FXM;e+Xz)vrxjq`f-gLLIxEceLWG zD+g|}6Bg5c#0Ik7Ba5&0l}#6yUnI?q;1Gg+>(+g6{g*t-0$9GCsZCqlN>4x#lJktp z65^tu!gP4Vs1143nud&-nP2tAWhwk{iK2GH(EEe%V?O$l#hLWb=OIg1J(J>w1&3ED zRM67+M<94xc45${(c=IlC$3zRqPTz@-s)grgO%KNq3FuLS3-6})W|Ie}9m}Ct zqd<3nO@E3p5okJ8)0OL+zRsg>FN8}JVg%e2x-}= z-KpY^$z1ukMy*r#y@fdF-y1tw{I=7~Wy-ou)^gaCGQK!ImeQd;9&@jtLK!$wh_&$N zm6pXm@bNDVw_R+y54Fr)-zu+OU9%U5H~Ho7t=VU3mRvIUk7GTSeOZ$C2*R+xc}Ect zH2Lu5)oGF3+w?uQAj=rZD2w!!DS6prZhTrgRMg@y3+7OvTYY^VyAR`@nYEevy3ZFG zKP2Ykv5D*EtDYVn5*I!_YSWcdklk-@+x?Y({gn))ndKkNs?QfO~>boA(s3r?H;MzjNnK*H13qhbK{+rI*;DSPDyWt|i5#kYkDWSz<=A;9TCbdOiJ_ z&(~!rIScJPKlK*@)2;1shBCcS=ML-UqvekGSgFMIKJS9o%P8WC$XWEC$@GZLgX@<( z6@~*<71JB%+vC?73a5aWX%r~$FEl|M9LgVoZij?pu|CZ)!H}(ynEFh*+iv4Z?Vo!> z0p)eRKcDCTMjz~NkL3VnFjq82at|@EP;FYH_tYedJ$yK{6)}XE$*1~M*@bwoxERgr zJa9RMjE$RqrT-(X(RG%A-Bvwrv8~5!rqYDFRTc2Vizf@!YE(M>%0zr{@?p<;cz9|| zH$P;E3ETL%Hmt-U+fcY6-I}eTkYpZP?dgi<3VVqDwDsw$hOw?=bmREhT*Uehd1jlo<**Hmk>#9mfQp-q~K&!4m$mH zXJ^meC(dY)kTP@^8(Ki2v68^V{oJ3qE+FLV@Tn}iXbYcKIoxd#JG9!Xik*?#vaqM* zXRI459qO+Ndbml0*-8>`n9d-oM7-d)d81{w0miL?`+ve>9!LMB$%s)U{na1GvZG%F z7>WsZIgEaeY($EnBW_HNRvfceqpPdiCHojuku1}jw6 zijI+Wic8zl?|rQz_*2=jbB6kwRj(#HhPC}9*cBZPZ{5BQp5wxtPM5&_b)lBpXcAgHb>M)IV@?;3q}R>lH)`Si^P$ARA|p^d7lOUNB| z-k~4#p&t~Ox8;MWjM>flC6*Y4L$~P7G9EK|>K_z*R>s#@j*iT=>32wr1F8_Ht0&vA z^)aZ;!m-_b3l1U0z1jL*7VbYExfeM!NYlaNxRp)ju}RzUEUwLGw>dvfM>91At-IGA z9>4+%S%Qk5*Pg7!*54HG&y=H#FYc$KV*%NS5iZ^Rk1nL+GHMm_I50pfZ%*kL`=WOW zc6uGhvSr%5&qdt#-g93Awq_@li##R<=CV8xJhWY$DhGzTZVaME1)E`6W7j?dMM?*T zxb>w30gnpz7O2~0g-!uh?*+N;OyIp{$W_J(GNglbK!x z?Qoa{!lUG+bf9z-cH^F~@f-1Rbev$bh1k5`R`TdlzVax;8ae{mGaUv_QFBw3)$(39 zA|8YN?JK8zD53A1C9@jP+aGX9G*|jDwu?@#I;-N7iCyoh%dCv-UdNkoe1l3N7Je~^ zpR+%oAQ(5MXumo?7F~kpVN2J;))~_UGIpy@yD}Fz7)gz(ms?=>NZ}Lw<^%CuhRtb0 zS=ie_0&LBYNE&ek?{^EcA25;#=+)WqfBqG`zHYW#fQi;b@QsSCNt*2q zEHRyZHxdYzQ!>hRR8X3at_DnUoplLPM(i<9+b)8~U%)ZF*=H(u=1L=xgq;;* z0O7gPW204~g>O_UxO)!A*2+vYIXlnSJ1jMsV(RTvm$7@Pi6vrnbbbd}{uqzLIkk=n zjdY9lS|Mv|?6*5n66i}?7)OZr>rQkUf6= zeTXks9>3`alfE1Btay8-E-Vqu)#SGYKPAOIg`PQd&Bn@DDn5sE>bm5c?aan{VH!T@ z&9jRXS-5_#+s@U~7;okh_H$pw%_B$uzU+Rx2fc-sE%7?t1X{ov63(tw{9DkpM5iME zu+&OaNkL&1eC{CLFVx1;q_`VR{)fbG*Ul#WUIE1dc>XW$Do{bW?Q-QB%Z%cN`QQ~w zpmz)ppOIjhV3)@;_{f#uKgQvXzbvQAJ{@YY+m4?a%ynG zE|&0YelNR=M|Z`fQ;jGj-9=&ZKvkK_EteGbTW!DK)jjZbtpOHiwF2~&AwMFWKvSRfsgo>odE#Tv6G&g)={h^*yHtof>MwM{D&8a?kN+H= za&h$7ZmHT0nh1X-R#K5^fcm1MR>AY z7^l3tq=hqK>vK*5!?*61!MrAa2(lbpej3TxR3!0f+O$d>E`XXFQ~6|mI$J3rL!~Fr zZ6LL&5CMRp@im^J8|ybl-)(wu@!fHtl_%4R#u5xI6<{TmeV9^ zbh=IuARf~viQL(0LO-6Nnb(@OjOh|o_wy2$v4vBXi|uHO^|Qh|C2~WC;#MQYMts_0 zDD(AGV$t3LJC*sACVRQfdxjZ#f3G*&sKqVEa-zhwTSk_RJ&${er*fqObxl;s7`mU; z+{$bgo#}$6o1D6R=T3ZXunAIg=jOjAlxEQaZ1pG$$Xl(;+bh=jbQ;Dbb~b@s^U%rQ z)^(0lJPC+RqUMw3^6{R`-jd@i=XDu_Fp`UnZ1548%t#@Fr%czep0a=%$! zTf?KLJN}gFLV&ZDXB_pZKn0%f*bVFrr6@BE&7M9_8`fR4cRsY#A@$zf-E26dwsAWA z=#OEcTmUOs*JYhhw<3zJXk+Gt+W!V%I_%nDmdN8f=YE`N7~7RD$^y#+b3|BJ}DUrHtCkemO{p zytGC+2!dxwmWIS(*5yj_O#6858ET>JcJtPD^O`A%&WJHNIevUZB=meKcKa@z)Y#Z&hN$x58k>&^6 zs^e~-S55HVej=_ej`5z4hYVZLdV@C+P(m(j+jL+tF|(EjN`iqy%@FLnxAe6P;|R6x*vPO}cj$S;b$IDU@5xRk^1*D1q%wn{qqu@}j~MN=|IV_~Uc z*dxX|+m4Ga!Vv~We*T1)cODSR1xUU%6Ur{|A{933?Tt~V-e4RQV@y7Cw5=|nqEY6( z9q4|1@^bA@1>V5t@*-9P^=Bh7*UEe`FE%fzUgg8iBXP{s15;xvQG7(vj<`;`F!=iK zs6H1q10wI#*r33?H(4;`kFkJVxIJLS>WVLaFLE(C;l`BKWnzh@y;lT zP(FfS%SHOOj+hwZb8=<@yP3D@YrjIs063N*-TCrpWnp1qXlMx72(+PNwalIxio=9&SwPf-yYLwNI)c=WxT*WYF^L0sC>9Dv;;suq^ua(mGcc*1$HW>F$Rl?btEC1^G zeI9G_B8SK|{|60Ff6(Bt*z9h2HOLp4*D>hgHhYoM=!)#4ReTIO@Q+$>E}L4AQq;EG z8x;TeQBzvn@`O;ImTQvIxJ_R#Ay3B5g38X8g~hfZ17zc>2bD z-66i0CrDL&-2FHe9J+u$z8re4;qE6P+Tw<7cUL5FSCGKE&4tkDk6sKmy?IkXw`;W$ ze=aIVOfw!u(De3U8=&Un<>XBOurub*{>jE*t^Sz1)XC?{-a@q;X5};?z_;!RBd^e_ z3tjISNf#x1@kO>UCxtvR((D(7ZVYp?1LHwlY zA_QzUnbC^8zG{M(3;*$QcbQ+hZeiv$9{OrP*K}Iy60epJ-$i!lb383FOz9f63x@9h zO`78N(^WVs3W0;K_aNMwMVpsvPo9vyWcxnir1j#qbfBnoJ}P>1BkxOuI(5L;4e*%~8{jQT&~jD)#d3W}lGe^Z@_IL7^i3juw@P_CTz!;J>^9hIr_k@qir#i?%PA4* z===MGd@tEHhf@<16Ft@uF#vdMZfSWqTNCtD*4rCh!rf;17}TS4iYiBn=MOKB!#p<= zhXBHqVzJ-Bm8D>%0zIb_admaFPQaJZs`I;kTQw*x z;hK;u9l!&`?itryZ7_$ymU+th5vA=A5+!{H>9=U@6z_JxnkAi$uBBRn(#4uL zY&vH}I~JN@hXuwC#uC*~+=w|?Xk_6uadR&I>44?53 zpD-+RI6W-#DY)+P!>`&Yj`4;N8q&^D1BqkzGetuU<23h^1ZQE#@ZsWApX$y(-&9y= zq2`#WACx2f8SN;;-GUM`sUNwoe_>uL^qjgph~aU8l&pZ@#EUbZXY!vjHuzA~)ool3 zjTY?Li8Gcc*(qc0U__ay_nOh$4LdAQM^Qni3TEDRbpGixREH_A^>{7`wMd#cw7#+N zV=MBT^xY}zoBFj@^iO-E>7{xB;@53=vazu-PcadIY1{#B&$T9|kfrk&!5DS6HyhX8 zRQbkvC&jJbR$YgU$(~dA&!Lu&0ag#Qh8j za|klYFn#h#XOwRGXOwHQa875uqs@@fv9+E1)=a6g>7Tk8FPn+4HFwJVA+g7>j&shC z2kfrXcH%~Fe1@y8L=y4SkR@BzuVDz>fe?sx3^s7yL=)8 z%@1D?&4>L#NVHf+F1MJkE$va0kHXiSm;o|6mgMrdj~3SKJFngT<6}833|*udv+`o0 zDoxmk zG#H1qBERIVj%BFzN4<+oU=uauy!IOe_VWR3Ca|@BFFN@Tv8D8=ZU_m_k_w8X7IjCT z?c{DJa_av`NdfqTKWS-H&DSGo!f{DSGj+B)BwM$NqSyHcb;@+BM3{8dD3RJ~x@}xd z&CI+@si;(yL>w$#2mA--dtSKjINWcY;##=hJ0X5yMB|<7^iF*jrC)Ei@)3|~v1@>@ zYJ1DDmgljI|2qo9owl4W?W47(fi_5Bb~^fR8@JYvlBr1aue|w=p!;9i*;4E!w0T{Z zqcKNQ?&=HgXmB;OXvg@G4GXAap{>nlx^k>!H(b%T&uDx(LfHqhx6l!#fBvVj)}t@4 z?Nn-9u|{^{di)HL7b{Zx5hUYAJe zTx2x4x1v(GWkwSfm!3)5c>;d+s}H|DtN}P`;rg6ro@zjfLP1ykag>t~$HBKz zqw6lkF#PyurA5mET$IWh?zd(Ca`P+f;NS#qI_wnZyN&z07AZ&CPkHiT4-a*nM`Hf` z_OKi2+sUx>v()JpZJ}K1x1M42!$jvU230K7CIlq2Y>$7{7=aJ6fv}%!Fs0!MP*Y|j zBN`8=)?RvEoFg~f*ZL2l&Yt2x*-)xK@IT-%o9oYgI7}e1qHJ(#a@a*alt|xcS{=K# zUuOBxK3!8?eKC~in`?J}m>e>^bjz0D#?GKIL=~CKA5)Wh>78D^P$|xi`RCt3*TgN1;m+Yjx1$6cFPhgb^>RmVo$mI`zkzUT+dhTB<_wbnzy(L;S1OxeLQaRLZR5qThD|2#T#<9t4!=P9qSyCF2brXMF%W!&!gUk=V(c3X|7rzA zt&J_4-?hXPOueYzIp5wtU%T*YT2&p5U-K2r09d#Wa1!eI@gI^N@6}>XSs?ebv$MBT z&r<~*V@EP)W3M@hpMU^j`oV(-fTSAM+kx1^U%}t`({~jr>n-+J3S;nuR26DP+@s|Zb zv{&icDAi0x|`GJR-SG%Y`KJUh(>bF`3d1p&f|hc$kA78PX?}Y zZ+s`IOfyT84)hcZWNjjM7ziWs=yVH+NurcGCN-S3746ULQY!`YXK`WW+m_E378V~q z3|;Kc1J=JR$sIg*x5$AU1Q1-a)>5B4uGWT$X=hGZ6NY+qrdzALE)P$i8a6prxHr2ro_iEI|slN-MtozQAkC#g< zH0C&J8Gp>(fw`sy=n*h}C8h&=ywib$FXN^iVR9R&D8DoAyt@?`qbZgQpWUK@j1?z} zmvm;IkUurDr0<5^B5zH&bIaqVKMSweQ{LBnEX;x88}~C1o~r6DTT2xUhf65_V56LBEqW-}M3=neK}yP_^QImpvG=j{`3lQ_wW zB|J<>^DbCl*H_3I2tXA=77^qs8zOkSBv#JGyjuMer>%whHHQy0(aVR(iS!PM<*T^X z-a2xe*XS%Y)kvt>QN6}Q5s_~5p!S@ylC3DyR5~L1LA$9%x;=+n1wLPEX&LXGzXItn zb8I*EIq%G?@pv9N2-_l^HB8xCh#pz_0EC+uv^Y>3UIW8%t`(ev{;Nar zvh8SUOm3$NjjGtTe6h58s?Ak+>VCeH%$c}#o>bGm_7ir->-aoFMdQ6$a5}oAqMf1a zHy|khOS--m1qK2=j$01fMQ#6-Ci7X2B=fOINJ!Arf1Q-IvDpP2G5|k>{Qdd0VaXdA zA-HroXp9>#Hxwn=$^i7>u)aV+nK8L2tn>F+4IHxv^KHx5o}S|NQ1Pn6qXmN;%fTjc zakIjlIy5`U?$w9&5Pybd{&WGz_({UxLI@FhX%%1`F{f}m8Uw~&p@wdwjUXYtk?&hvS9@aLdx<3w@Czq9eIO~>l}c?F8f;^j|^7d&^iv+*BaZ%@Sq zc|cx~i`|1SAO?T%Xpq?h2`9%Ui#7pFloFG8mq!2|80~%Nx}J6|#n_gGsxoB^){q{OQEqOjGoYnjkA(#88$aso zK=y$bV(=zESXgUp<&9)?9u&WFDk^qc$<$9lZYqCWV=A`orZ~K%CSpH=TRYWZ7B`o> ztgdi+%Hg9#l5DwtS+F!5eKQsP{Shcu#>ng}WKqm#CxiBf89q8%{FS8F)1E@^vEAn3 z*geT((K!`W<)>8OI9VLw7uPSBV}T=QSMrgxvGk!s6_tRz!5iN69!v2`KR{GejvFZ~ zyJv`c9L!Xi0wFjoUR7SF7HC;p9QG=XMWsHBNUt3z50=_e>k$AXp%&0iN!AhbZZ(*O zZ0_PS@st#=Sz$Gu6~)13s?7_So^mnq!M;H z+a4ohQBITaZJtkK0gL?c{9QrN-aLov*&(HE+r|n%|Ng;gH8q(nLY7MucD)5clD^U8 z$bJ)MfHObi1tN)=J&lZvTwGk(_fht#rLc_^du|*YV2`Yj-T3OJIY+_sOJ28)>T~2Y zM2IY-<4qhuxfyU7QzZXD7KL=c8yF%9gw<@0*q3v`b1QVoWkxOTN(!O-?9WJEe*Fy^ zB`w@I1qJ@xDl<^s+P5JfZ(Y7BiOg93y7F-Goh!bv+IdYD9ZN@CjII17J@|tYkc2+~ z67h@zxQ3tvhs30oY3*hz{iag1jtRF$jKv&r*-f;*T1-2?8|>R{#JqMdoymH`;VpdR)Dqlnol~X8j`@C8RB2VJGY(-QQP#sB-&cP)6iu z-Q|^ufk=I2c6?`$vs&m=5&!CyG+_uc5K{~U`TCe-qZT3=%1?4= z)Z&YkVw$GW1FvSuFiMOl7*~q=u4J#!YQ%{&$bnrwr)>n>x*$$+#iO{1#DY}3{agnZ zf^B2VE4EWqSy{sM3f(w0dq`yDb2)zAo#?3QWqFkCEtS4p!5;m<50EXur!^;BBfIBn zJYU}5yy@29dwSK2IE;(?5sRbS_~p)g%a3J{Tc;>(xMR|7K+NGXVhxnjaM@(Q-JxEF z`r;_=LnE7cxgMt?;&Jf7n0K;g6fPY85&TX2sU^I=_zV}y%MmCEQC^fl> zQ@)X$nDoCS84K6+6gt1MwA#N$xjXqCIT0Fj=q1}BDfQ-C?ScW^M$Q8 z;xQct_mqu~uS|OsH`eoVIx!!eZ!*cvty_n%l4eNwqJcQ&_>=qhV(6(FSGum&piksl zf4t7MQT86%AY_qY(Q^6Ir*1ipm`1#H6$$aeIVp-Xr9}ZODG0@ z=u;@G>h$EMTqM;JfUfjLPy#WNN_;Hj?R&IF!wdJ`aQMeR2x&9;f`m2w5%hmN7vUvA#VI3|AK{g*lOsj2H3(;}f= z@>2eTwB2UMQ~@rdP3IKTLM0z+^6z! zRFvZSm(54nO8uV9HRfv^(dUT-B{ms0dBNDt<=K(!EWH68DXi`isy zp>t55(ok}e_ZxmjQBy56yE_4$)Bn{lVrAZeZ*>8-9tT4T@86#pJm)f?q6K#x%SFr* zfBD&os(-WozdI<@|N4H0>1XH9>fS-iX>w`<6D14tf8JZp4=% zZ)TKcI@N>HTTbE6XXpoBoUXTdGlV)zf7dcsHI|kZstCN(-cZ&!Pk!I!U;g*Z-~G2z z2tw)ueqhFE>BXIwAM9EZlEo3SAQ?>m%t`UE1);+;43SeJQvk-TTccePDW1wQFsHV^z%qYf2Vk+h}UFRY@6h(6`tm-<(cq>>T8m{(w1v-EG@S%h2 z-Tx(|nO&m6%|TAX=HUMhk44P!kiK}+rmUGXxirw)-rd%IdEIkLex?9=tIOj=skQpw zOh6+^I7EO(+7&6g?k0(7FicXMC!M5=PbKGA%g-P(HcPUzHS1D-u3HrsyD<*hBo@hu zJv>uX&?;kd&`S)Ie|z6oLqzGOaFYGHX8LezG^%A0+X;~K-mFN`YrI>v7^S*o>F zH;w#LqTk`%#te6(5|bkb*=yITr4J?Xr750NZJ>Zz^a@m8=Gn7eqffRE|Mw7C|95nN#5rlN&9V&d#p9QzL&rSL zh;7Om)87lFS%lScuSP&sFN6L;n9T^Q#2IBb-nIB$qWE0Peg0Hyw>A~|&}<8rH@tZc zmd!Q*oT($I!&>V3RCV#WwxB@PP~SJsh%ljro!4IaZJHl?6+S_vG>4zTGt|6*!)ck~vv1-?wU&y^&b|z*S6d4V^sQg|^lNN!QK?jO zsQ2mgD>~_p!rND%J7nz*dGzE!Zt)jlKtls!9F-&!2$G9=oc^rL@5OY4C=ERUFS5Cj zqvhr!p-Its!vAx`O#UY1=)mK6{02Rfqn;J~f4#kRSd?2AH%v))51kT9jUXXNkJ8zU8N-2^xPM&bess*gPKFI0nqudHoaWcCX`E8J*wHpmT0i@O#uNtnLLi@ux$tWGSP zF!HBlU7e&)2O9a=)}$1-d#9!zO*L#cG#b2mrjYiK%Z3`kyx*UL-sUpvdP75AO@1UI z?}Om0=Z}P4_6B>+9N-b>BjM*m;E!k^ZfC7$7oA94&KA%q4fZ_KO?VvkH`C@1wEuOx zMg5d(CPod8PPadO>Y^90_=hESFnC6Wc7|>SJI#IS6#MYu1IQnL-tFkhWs9^B^e0`7 zD;JR3sbN{Ix~*7{lzgYt>#9uk`E+?7o#!7QizMR6NMvy|4q10l5iGc^cH8f`YUw9i zlYKkuXn#NQ^VtcTF(yt<>*M_t)L0{NwvMlPWKU{e*%kWSG{m<29RF_QtqcC@mv~g9 zAMEbO5Vm~zj8VnKrGrqEY>W)rA}2`9|9AK>anG<;uVEdB$S% z;eLVmQ2w282Upp==&GPqGzq-4_MjI?nas@r(5T#6JEM!ct7Ct3nJ3NvC&Pw+15Z{E*-ID{gd6@lf-`^ z=opJQVS47G@3d9Q8;KypXNH@4lB+}0t2df^>m_H$KZJ&CFXBKO|dPFV@PXbvwuNG-QXzEj2= zADJ6&1y|3^D2gjiyp>{i6h=JIt5z~SWUCmXq~*I+ERv0H`)&-VO{!B1efeUPUon zZoc!&UcI6uGwUcG+_5gaZpEl6rdCOXFV_X!_DVHvHJH!AJkxmt2njHhHb0LO(4$HutZdf=V;IQridScD7EpD3T?Nk>2Q(Jp?9)vum(x z?=?yVNLVdpWbEi(M#M<8u7q@G{ZRjj1kOW`fy;$jK_jPVIK&XsR)<4DBMTdoT_C(}({tBt0HO~_3`G`jfpQCLHc=~Uh&j4;heLC_DgF^5g6 z1Vld?r|Kz8CJ8Ej=|BH2My1R5U+NY;b<~NZ8yG)7p+-amwE>|a#E?u< zbC>Fr?dMEo!!7#L1dJwX_0(&~L%AO79?N#K(#o0aUmLpT`N{Fuh~qDrK~m*d`0W5n zne!8;h2VoT*naa0QXlSbdr9OPgB&vOX#nmAuWheQkUd8dMnX=?6><(9&JmL0Vx>F~ zm^>9%RduV%Y1+x<;j@gSiw*`87ytS7h{gOrM^AsPCH{;J&Sc*w?5*~|@a5%Kf?o>x zezlOgmwO~XqANpj{az*OrWXQ7WboQ--2MCFuIobt;4xX>!@mCY?LNPwoFlXYv(DE0jcPn|u%{jne(4VqZ#On*KPr_@7RqXBj_q$;z+h?q zm=1UTpo=4Gs(TrK24`28h7s+D`~er{FHw5yqCI?`7^1261$!%Ydp%&g=hh6>y%&$4 zQMNGWs;k2{SHjGmX^V`DI~ULi zCV!zxa{9C|R1_q?cz1J5sT4IDl6#mWk~mBjV|uiiBq`kfa~yZ-R}PxIAJEMZ6y_+on_G~_i-sru#o2O}{Ig1nhwiYXo3pGIIzE&18?DXsp#)c+raAW} z?ZJAgvHUbj6xbY@(wp0b~j?2<^`mw2D3sWFZO+rTO zwWC`2jap3PyP;&3N=kfI$9>N(KNi_-KZ+4o9 z@{xi=rrKTzkz()botm)Am*k8YE@KmT>Qq})vKmUe(0gNmLTjFO z8hAe;!yQRPD(WE;R6TxXr=lq+e+${b;D{Cw292v=}F>Xs;+2xrN&~}@nuSFl%$jr zdwi1m=H>}QDLut>Q|m)`=e%}qbo+C|?5_s}9M!0z%p_1T$$)SB1G@VG4#GD(sSR0%Y-qf(YumepGR?Rfq^u)B6d8jpzo4Q3F z|KuQ|lfm4}5Bn!|^Sk8&6rAdK_>=3s67`zb6+|b(VtQ+pnMtK7;_+QtjXlaJ+XM_n z(jU#bVjFsS%73DN&+adTuIZQ(kehGEKyX2Dl6KZeR4ibRMx$HWF}GXW$?J}trAb6T zV8E$>{*QL>BO*!*K8@0tc_|?w*6CR6thyThW`QL%s(zs&6qTCzDS8VxoE&i4+g{7Qw7y%fLDTxQZ6}|7y)o)ibcF4I6z0@j3B% z)N=0}p54FYvbK^I*F8^3tw#gVBZS-%hHfg3HFPYWT~kyPOHGZ*sN%ds_N$5Zyc7)! zffF?5`^LeaRM$I}fix88=N7tr_wI5ORSh+gumA5MyJM^*3pracI92Z`-mkoRML;XE z-$6hfl_``uw@pc3W%X6}8Pf7`!>^f6! z&i@o=DtZo47SLx~l3VX(KDk$-sI)wmn@!1qPDJ#gH>4Reao>Tm=wh)X_IpUd#dA?& zo1~1;zHj>A8+?Ox9HoH3MgE(J(T}*sMMQfH^`-;sEgWVo^$;*$QBRP)MZd!cZOd~k zI~0wqGhB(LMV)CZ)r{bZdS{S385;c$SMwWc6rJ4Q{U(#01-jpq+Y{2K^JVOk)>S!0 zrDvb5ue*W~R5s(cULQ0aY)&{BR`lMa#dtm}3ri!j(_|x&m#SyE)%Bu+16jjba5FLk zN~F0R_Qdpwad8B)oA9CTr_x-X>%V$3bv6rP5UTl1^XNRwp}SR$>rB!CBlOJAWjC43 zY^9XntjmhQx(`yKh4|fGpE5&pr#pid3Lp+8pT7Y1=mK}VPwt^A(aG%G+{CLX54|?sy5rz{-Q8f99R?yL zU21~3d+4f!Za$5rK~#}}UmKZT8t=uvVgC6Q71~$#(2@QO7)+4Op=D=^V>6RkcSy!> zsgs2benGV8@xs=puM`%#DrXAbQ#Wy*W-pbucZ90hC64V>cyP@9ij`g~UXY=y)>`}J6LFE7N zOsuA!!a_EPg}hsOt@FrwLM{dy!A{C-&1j!;S)9Q`-Qk?+Jqx8I0lUPpQd!e*p2WMu zPI6b&DSuMHA878;jdFPvisjM+B2tKj6#-mfZ6_1J4K8d#;{S zG|;PsW(%1}4*NB2GgtjM>YI6`v0#$AoYuzH;5%aeO6n&Ao6yrh>Q>+0cRz?XVREuU}mRa?o*0R zwnF-IUp@~+cx_x{dfcA{>B9(9QF$}vVA=xX0<}K{D))sMrZ-%mq=DJ5zWCrE-`&H_>J=J#OHi83`PMpPc$|n@5hPJ0 zea=U!U))VRGahTS`eV~(7g{ZcURFPTjP(BM*`EGj9Yc%rVP@58-Bs|FS(5c}z77y( zh4pxfB?En&Y3_LtdcP*jBBm!RY)?lw$@~hN`ITog6Gx+mZu5m752CFGXWYFX+>Jwj z9UOz4a-g_VB$A@08Tgh>3Ns1+D+ut95W(w3WM?eC7)Et?)g4Yd53a9oO-;Gk9|tTd zLrNd7JiE5p;Ew9GH1d4rK3k_MrUvZYy>Z$C^LyE6vOhYLgf+jfXqe3kN;p*S-#_tN zP9!-x6QMY5vj59K(7atzT{S8CO!4KbjHqX%IoP>TKEH_#6}KwjPcS3$t;-jv`y7=nnH+%kP3Zd+%?H$K!d7SaSVNOP_Cy{kkK$X~mQYPZ)Kg&azHTsQI1Wf5 zR<9eWw;Gi#lf`2$?d^d^7`NEA6!+6CUvgb-Hus9`(kf~6nn zZ4-PsXhV*JEa?MDP}#;U!lvA==^Dt*BBh$4<6BbZR{F;II@jR}WM-6H1xpsUvd?fY zr+SetS9>-lC@9;=78*aRXyC%kKA^0qv(M!>%4`m~J2A^MN^%}pRd7VnjfK5fp*c7? zATea-HouZrdK{4&+fK;;>8L;3$zWgnVBSO)W4t$@oE_J7E?XYWNo#{dAWrpasH#m^ zidc||MfF2h>lf|BdRqcU7}*4;hcO9C40*9rqIoMguSI?PaY3q|6@lB7@&R{oLl
7+Nk#p(hAzK;423@`!KK&CQ$qjy4=ed8X>_ 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 )4hBWw7lpkUDN)bH_9zl6e-z)tK-=cFsYw=S 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 Ib=~j(02K=XaR2}S literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.tacmi/pom.xml b/bundles/org.openhab.binding.tacmi/pom.xml new file mode 100644 index 0000000000000..aa3ce0d7c5050 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.9-SNAPSHOT + + + org.openhab.binding.tacmi + + openHAB Add-ons :: Bundles :: TA C.M.I. Binding + + + + org.attoparser + attoparser + 2.0.5.RELEASE + compile + + + diff --git a/bundles/org.openhab.binding.tacmi/src/main/feature/feature.xml b/bundles/org.openhab.binding.tacmi/src/main/feature/feature.xml new file mode 100644 index 0000000000000..b7a38c94d4f59 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.tacmi/${project.version} + + diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java new file mode 100644 index 0000000000000..d8b71ef31bf3b --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; + +/** + * The {@link TACmiBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiBindingConstants { + + public static final String BINDING_ID = "tacmi"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_CMI = new ThingTypeUID(BINDING_ID, "cmi"); + public static final ThingTypeUID THING_TYPE_COE_BRIDGE = new ThingTypeUID(BINDING_ID, "coe-bridge"); + public static final ThingTypeUID THING_TYPE_CMI_SCHEMA = new ThingTypeUID(BINDING_ID, "cmiSchema"); + + public static final ChannelTypeUID CHANNEL_TYPE_COE_DIGITAL_IN_UID = new ChannelTypeUID(BINDING_ID, + "coe-digital-in"); + public static final ChannelTypeUID CHANNEL_TYPE_COE_ANALOG_IN_UID = new ChannelTypeUID(BINDING_ID, "coe-analog-in"); + + public static final ChannelTypeUID CHANNEL_TYPE_COE_DIGITAL_OUT_UID = new ChannelTypeUID(BINDING_ID, + "coe-digital-out"); + public static final ChannelTypeUID CHANNEL_TYPE_COE_ANALOG_OUT_UID = new ChannelTypeUID(BINDING_ID, + "coe-analog-out"); + + public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_SWITCH_RO_UID = new ChannelTypeUID(BINDING_ID, + "schema-switch-ro"); + public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_SWITCH_RW_UID = new ChannelTypeUID(BINDING_ID, + "schema-switch-rw"); + public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID = new ChannelTypeUID(BINDING_ID, + "schema-numeric-ro"); + public static final ChannelTypeUID CHANNEL_TYPE_SCHEME_STATE_RO_UID = new ChannelTypeUID(BINDING_ID, + "schema-state-ro"); + + // Channel specific configuration items + public final static String CHANNEL_CONFIG_OUTPUT = "output"; +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java new file mode 100644 index 0000000000000..7aacbd3553d3c --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiChannelTypeProvider.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.type.ChannelType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeProvider; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.osgi.service.component.annotations.Component; + +/** + * Provides all ChannelTypes for the schema binding... + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +@Component(service = { TACmiChannelTypeProvider.class, ChannelTypeProvider.class }) +public class TACmiChannelTypeProvider implements ChannelTypeProvider { + + private final Map channelTypesByUID = new HashMap<>(); + + @Override + public Collection getChannelTypes(@Nullable Locale locale) { + return Collections.unmodifiableCollection(channelTypesByUID.values()); + } + + @Override + public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) { + return channelTypesByUID.get(channelTypeUID); + } + + public ChannelType getInternalChannelType(ChannelTypeUID channelTypeUID) { + return channelTypesByUID.get(channelTypeUID); + } + + public void addChannelType(ChannelType channelType) { + channelTypesByUID.put(channelType.getUID(), channelType); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java new file mode 100644 index 0000000000000..1f17d06221711 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal; + +import static org.openhab.binding.tacmi.internal.TACmiBindingConstants.*; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.net.http.HttpClientFactory; +import org.openhab.binding.tacmi.internal.coe.TACmiCoEBridgeHandler; +import org.openhab.binding.tacmi.internal.coe.TACmiHandler; +import org.openhab.binding.tacmi.internal.schema.TACmiSchemaHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link TACmiHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.tacmi", service = ThingHandlerFactory.class) +public class TACmiHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet( + Stream.of(THING_TYPE_CMI, THING_TYPE_COE_BRIDGE, THING_TYPE_CMI_SCHEMA).collect(Collectors.toSet())); + + private final HttpClient httpClient; + private final TACmiChannelTypeProvider channelTypeProvider; + + @Activate + public TACmiHandlerFactory(@Reference HttpClientFactory httpClientFactory, + @Reference TACmiChannelTypeProvider channelTypeProvider) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.channelTypeProvider = channelTypeProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_CMI.equals(thingTypeUID)) { + return new TACmiHandler(thing); + } else if (THING_TYPE_COE_BRIDGE.equals(thingTypeUID)) { + return new TACmiCoEBridgeHandler((Bridge) thing); + } else if (THING_TYPE_CMI_SCHEMA.equals(thingTypeUID)) { + return new TACmiSchemaHandler(thing, httpClient, channelTypeProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiMeasureType.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiMeasureType.java new file mode 100644 index 0000000000000..f2f08a4365479 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiMeasureType.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This enum holds all the different measures and states available to be + * retrieved by the TACmi binding, including the scale factors needed to convert the received values to the real + * numbers. + * + * @author Timo Wendt - Initial contribution + * @author Wolfgang Klimt - improvements + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public enum TACmiMeasureType { + NONE(0, 1), + TEMPERATURE(1, 10), + UNKNOWN2(2, 1), + UNKNOWN3(3, 1), + SECONDS(4, 1), + UNKNOWN5(5, 1), + UNKNOWN6(6, 1), + UNKNOWN7(7, 1), + UNKNOWN8(8, 1), + UNKNOWN9(9, 1), + KILOWATT(10, 100), + KILOWATTHOURS(11, 10), + MEGAWATTHOURS(12, 1), + UNKNOWN13(13, 1), + UNKNOWN14(14, 1), + UNKNOWN15(15, 1), + UNKNOWN16(16, 1), + UNKNOWN17(17, 1), + UNKNOWN18(18, 1), + UNKNOWN19(19, 1), + UNKNOWN20(20, 1), + UNKNOWN21(21, 1), + + UNSUPPORTED(-1, 1); + + private final int typeval; + private final int offset; + + private static final Logger logger = LoggerFactory.getLogger(TACmiMeasureType.class); + + private TACmiMeasureType(int typeval, int offset) { + this.typeval = typeval; + this.offset = offset; + } + + public int getTypeValue() { + return typeval; + } + + public int getOffset() { + return offset; + } + + /** + * Return measure type for a specific int value + */ + public static TACmiMeasureType fromInt(int type) { + for (TACmiMeasureType mtype : TACmiMeasureType.values()) { + if (mtype.getTypeValue() == type) { + return mtype; + } + } + logger.debug("Received unexpected measure type {}", type); + return TACmiMeasureType.UNSUPPORTED; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodData.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodData.java new file mode 100644 index 0000000000000..134eb7728a802 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodData.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.tacmi.internal.message.Message; +import org.openhab.binding.tacmi.internal.message.MessageType; + +/** + * This class carries all relevant data for the POD + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class PodData { + protected final byte podId; + protected final MessageType messageType; + protected @Nullable Message message; + + /** + * Create new AnalogValue with specified value and type + */ + public PodData(PodIdentifier pi, byte node) { + this.podId = pi.podId; + this.messageType = pi.messageType; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodDataOutgoing.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodDataOutgoing.java new file mode 100644 index 0000000000000..16bffcc77e33b --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodDataOutgoing.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.openhab.binding.tacmi.internal.message.AnalogMessage; +import org.openhab.binding.tacmi.internal.message.DigitalMessage; +import org.openhab.binding.tacmi.internal.message.MessageType; + +/** + * This class carries all relevant data for the POD + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class PodDataOutgoing extends PodData { + + protected long lastSent; + protected final ChannelUID[] channeUIDs; + protected final boolean[] initialized; + private boolean allValuesInitialized; + + /** + * Create new AnalogValue with specified value and type + */ + public PodDataOutgoing(PodIdentifier pi, byte node) { + super(pi, node); + boolean analog = pi.messageType == MessageType.ANALOG; + int valueCount = analog ? 4 : 16; + this.channeUIDs = new ChannelUID[valueCount]; + this.initialized = new boolean[valueCount]; + this.allValuesInitialized = false; + this.message = analog ? new AnalogMessage(node, pi.podId) : new DigitalMessage(node, pi.podId); + this.lastSent = System.currentTimeMillis(); + } + + /** + * checks if all (in use) values have been set to a value - used to prevent sending of unintended values via CoE + */ + public boolean isAllValuesInitialized() { + if (this.allValuesInitialized) { + return true; + } + boolean allInitialized = true; + for (int idx = 0; idx < this.initialized.length; idx++) { + if (this.channeUIDs[idx] != null && !this.initialized[idx]) { + return false; + } + } + if (!allInitialized) { + return false; + } + this.allValuesInitialized = true; + return true; + } + + public String getUninitializedChannelNames() { + if (this.allValuesInitialized) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + for (int idx = 0; idx < this.initialized.length; idx++) { + ChannelUID ct = this.channeUIDs[idx]; + if (ct != null && !this.initialized[idx]) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(ct.getId()); + } + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodIdentifier.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodIdentifier.java new file mode 100644 index 0000000000000..3a718a160df2e --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/PodIdentifier.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.tacmi.internal.message.MessageType; + +/** + * This class defines a key for POD identification + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public final class PodIdentifier { + public final MessageType messageType; + public final byte podId; + public final boolean outgoing; + + /** + * Create new AnalogValue with specified value and type + */ + public PodIdentifier(MessageType messageType, byte podId, boolean outgoing) { + this.messageType = messageType; + if (podId < 0) { + throw new ArrayIndexOutOfBoundsException(podId); + } + switch (messageType) { + case ANALOG: + if (podId < 1 || podId > 8) { + throw new ArrayIndexOutOfBoundsException(podId); + } + break; + case DIGITAL: + if (podId != 0 && podId != 9) { + throw new ArrayIndexOutOfBoundsException(podId); + } + break; + } + this.podId = podId; + this.outgoing = outgoing; + } + + @Override + public int hashCode() { + return (this.messageType.ordinal() << 8) | podId | (outgoing ? 0x10000 : 0); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof PodIdentifier)) { + return false; + } + PodIdentifier po = (PodIdentifier) o; + return this.messageType == po.messageType && this.podId == po.podId && this.outgoing == po.outgoing; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfiguration.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfiguration.java new file mode 100644 index 0000000000000..3b5b1edf46c1a --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfiguration.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link TACmiConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiChannelConfiguration { + + /** + * chnanel / output id + */ + public int output; + + // required for MAP operations... + @Override + public int hashCode() { + return output; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || !other.getClass().equals(TACmiChannelConfiguration.class)) { + return false; + } + TACmiChannelConfiguration o = (TACmiChannelConfiguration) other; + return this.output == o.output; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationAnalog.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationAnalog.java new file mode 100644 index 0000000000000..215ae65278018 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationAnalog.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link TACmiConfiguration} class contains fields mapping thing + * configuration parameters. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiChannelConfigurationAnalog extends TACmiChannelConfiguration { + + /** + * measurement type + */ + public int type; + + /** + * initial value + */ + public @Nullable Double initialValue; + + // required for MAP operations... + @Override + public int hashCode() { + Double iv = initialValue; + return 31 * output * type * (iv == null ? 1 : iv.hashCode()); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || !other.getClass().equals(TACmiChannelConfigurationAnalog.class)) { + return false; + } + TACmiChannelConfigurationAnalog o = (TACmiChannelConfigurationAnalog) other; + return this.output == o.output && this.type == o.type && Objects.equals(this.initialValue, o.initialValue); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationDigital.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationDigital.java new file mode 100644 index 0000000000000..cc57e6cae517a --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiChannelConfigurationDigital.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link TACmiConfiguration} class contains fields mapping thing + * configuration parameters. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiChannelConfigurationDigital extends TACmiChannelConfiguration { + + /** + * initial value + */ + public @Nullable Boolean initialValue; + + // required for MAP operations... + @Override + public int hashCode() { + Boolean iv = initialValue; + return 31 * output * (iv == null ? 1 : iv.booleanValue() ? 9 : 3); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || !other.getClass().equals(TACmiChannelConfigurationDigital.class)) { + return false; + } + TACmiChannelConfigurationDigital o = (TACmiChannelConfigurationDigital) other; + return this.output == o.output && Objects.equals(this.initialValue, o.initialValue); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiCoEBridgeHandler.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiCoEBridgeHandler.java new file mode 100644 index 0000000000000..8880f9c796a06 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiCoEBridgeHandler.java @@ -0,0 +1,248 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.tacmi.internal.message.AnalogMessage; +import org.openhab.binding.tacmi.internal.message.DigitalMessage; +import org.openhab.binding.tacmi.internal.message.Message; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link TACmiCoEBridgeHandler} is the handler for a smarthomatic Bridge and + * connects it to the framework. All {@link TACmiHandler}s use the + * {@link TACmiCoEBridgeHandler} to execute the actual commands. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiCoEBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(TACmiCoEBridgeHandler.class); + + /** + * Port the C.M.I. uses for COE-Communication - this cannot be changed. + */ + private static final int COE_PORT = 5441; + + /** + * Connection socket + */ + private @Nullable DatagramSocket coeSocket = null; + + private @Nullable ReceiveThread receiveThread; + + private @Nullable ScheduledFuture timeoutCheckFuture; + + private final Collection registeredCMIs = new HashSet<>(); + + public TACmiCoEBridgeHandler(final Bridge br) { + super(br); + } + + /** + * Thread which receives all data from the bridge. + */ + private class ReceiveThread extends Thread { + private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class); + + ReceiveThread(String threadName) { + super(threadName); + } + + @Override + public void run() { + final DatagramSocket coeSocket = TACmiCoEBridgeHandler.this.coeSocket; + if (coeSocket == null) { + logger.warn("coeSocket is NULL - Reader disabled!"); + return; + } + while (!isInterrupted()) { + final byte[] receiveData = new byte[14]; + + try { + final DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + coeSocket.receive(receivePacket); + } catch (final SocketTimeoutException te) { + logger.trace("Receive timeout on CoE socket, retrying ..."); + continue; + } + + final byte[] data = receivePacket.getData(); + Message message; + if (data[1] > 0 && data[1] < 9) { + message = new AnalogMessage(data); + } else if (data[1] == 0 || data[1] == 9) { + message = new DigitalMessage(data); + } else { + logger.debug("Invalid message received"); + continue; + } + logger.debug("{}", message.toString()); + + final InetAddress remoteAddress = receivePacket.getAddress(); + final int node = message.canNode; + boolean found = false; + for (final TACmiHandler cmi : registeredCMIs) { + if (cmi.isFor(remoteAddress, node)) { + cmi.handleCoE(message); + found = true; + } + } + if (!found) { + logger.debug("Received CoE-Packet from {} Node {} and we don't have a Thing for!", + remoteAddress, node); + } + } catch (final IOException e) { + if (isInterrupted()) { + return; + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error processing data: " + e.getMessage()); + + } catch (RuntimeException e) { + // we catch runtime exceptions here to prevent the receiving thread to stop accidentally if + // something like a IllegalStateException or NumberFormatExceptions are thrown. This indicates a bug + // or a situation / setup I'm not thinking of ;) + if (isInterrupted()) { + return; + } + logger.error("Error processing data: {}", e.getMessage(), e); + } + } + } + } + + /** + * Periodically check for timeouts on the registered / active CoE channels + */ + private void checkForTimeouts() { + for (final TACmiHandler cmi : registeredCMIs) { + cmi.checkForTimeout(); + } + } + + @Override + public void initialize() { + try { + final DatagramSocket coeSocket = new DatagramSocket(COE_PORT); + coeSocket.setBroadcast(true); + coeSocket.setSoTimeout(330000); // 300 sec is default resent-time; so we wait 330 secs + this.coeSocket = coeSocket; + } catch (final SocketException e) { + // logged by framework via updateStatus + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Failed to create UDP-Socket for C.M.I. CoE bridge. Reason: " + e.getMessage()); + return; + } + + ReceiveThread reciveThreadNN = new ReceiveThread("OH-binding-" + getThing().getUID().getAsString()); + reciveThreadNN.setDaemon(true); + reciveThreadNN.start(); + this.receiveThread = reciveThreadNN; + + ScheduledFuture timeoutCheckFuture = this.timeoutCheckFuture; + if (timeoutCheckFuture == null || timeoutCheckFuture.isCancelled()) { + this.timeoutCheckFuture = scheduler.scheduleWithFixedDelay(this::checkForTimeouts, 1, 1, TimeUnit.SECONDS); + } + + updateStatus(ThingStatus.ONLINE); + } + + public void sendData(final byte[] pkt, final @Nullable InetAddress cmiAddress) throws IOException { + final DatagramPacket packet = new DatagramPacket(pkt, pkt.length, cmiAddress, COE_PORT); + @Nullable + DatagramSocket sock = this.coeSocket; + if (sock == null) { + throw new IOException("Socket is closed!"); + } + sock.send(packet); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + // just forward it to the registered handlers... + for (final TACmiHandler cmi : registeredCMIs) { + cmi.handleCommand(channelUID, command); + } + } else { + logger.debug("No bridge commands defined."); + } + } + + protected void registerCMI(final TACmiHandler handler) { + this.registeredCMIs.add(handler); + } + + protected void unregisterCMI(final TACmiHandler handler) { + this.registeredCMIs.remove(handler); + } + + @Override + public void dispose() { + // clean up the timeout check + ScheduledFuture timeoutCheckFuture = this.timeoutCheckFuture; + if (timeoutCheckFuture != null) { + timeoutCheckFuture.cancel(true); + this.timeoutCheckFuture = null; + } + + // clean up the receive thread + ReceiveThread receiveThread = this.receiveThread; + if (receiveThread != null) { + receiveThread.interrupt(); // just interrupt it so when the socketException throws it's flagged as + // interrupted. + } + + @Nullable + DatagramSocket sock = this.coeSocket; + if (sock != null && !sock.isClosed()) { + sock.close(); + this.coeSocket = null; + } + if (receiveThread != null) { + receiveThread.interrupt(); + try { + // it should join quite quick as we already closed the socket which should have the receiver thread + // caused to stop. + receiveThread.join(250); + } catch (final InterruptedException e) { + logger.debug("Unexpected interrupt in receiveThread.join(): {}", e.getMessage(), e); + } + this.receiveThread = null; + } + super.dispose(); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiConfiguration.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiConfiguration.java new file mode 100644 index 0000000000000..387d4c27fb08c --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link TACmiConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiConfiguration { + + /** + * host address of the C.M.I. + */ + public @Nullable String host; + + /** + * CoE / CAN node ID we are representing + */ + public int node; +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiHandler.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiHandler.java new file mode 100644 index 0000000000000..f0615e047914c --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/coe/TACmiHandler.java @@ -0,0 +1,405 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.coe; + +import static org.openhab.binding.tacmi.internal.TACmiBindingConstants.*; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +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; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.tacmi.internal.TACmiBindingConstants; +import org.openhab.binding.tacmi.internal.TACmiMeasureType; +import org.openhab.binding.tacmi.internal.message.AnalogMessage; +import org.openhab.binding.tacmi.internal.message.AnalogValue; +import org.openhab.binding.tacmi.internal.message.DigitalMessage; +import org.openhab.binding.tacmi.internal.message.Message; +import org.openhab.binding.tacmi.internal.message.MessageType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link TACmiHandler} is responsible for handling commands, which are sent + * to one of the channels. + * + * @author Timo Wendt - Initial contribution + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public class TACmiHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(TACmiHandler.class); + + private final Map<@Nullable PodIdentifier, @Nullable PodData> podDatas = new HashMap<>(); + private final Map<@Nullable ChannelUID, @Nullable TACmiChannelConfiguration> channelConfigByUID = new HashMap<>(); + + private @Nullable TACmiCoEBridgeHandler bridge; + private long lastMessageRecvTS; // last received message timestamp + + /** + * the C.M.I.'s address + */ + private @Nullable InetAddress cmiAddress; + + /** + * the CoE CAN-Node we representing + */ + private int node; + + public TACmiHandler(final Thing thing) { + super(thing); + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + + scheduler.execute(this::initializeDetached); + } + + private void initializeDetached() { + final TACmiConfiguration config = getConfigAs(TACmiConfiguration.class); + + if (config.host == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!"); + return; + } + try { + cmiAddress = InetAddress.getByName(config.host); + } catch (final UnknownHostException e1) { + // message logged by framework via updateStatus + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Failed to get IP of CMI for '" + config.host + "'"); + return; + } + + this.node = config.node; + + // initialize lookup maps... + this.channelConfigByUID.clear(); + this.podDatas.clear(); + for (final Channel chann : getThing().getChannels()) { + final ChannelTypeUID ct = chann.getChannelTypeUID(); + final boolean analog = CHANNEL_TYPE_COE_ANALOG_IN_UID.equals(ct) + || CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct); + final boolean outgoing = CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct) + || CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(ct); + // for the analog out channel we have the measurement type. for the input + // channel we take it from the C.M.I. + final Class ccClass = CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(ct) + ? TACmiChannelConfigurationAnalog.class + : TACmiChannelConfigurationDigital.class; + final TACmiChannelConfiguration channelConfig = chann.getConfiguration().as(ccClass); + this.channelConfigByUID.put(chann.getUID(), channelConfig); + final MessageType messageType = analog ? MessageType.ANALOG : MessageType.DIGITAL; + final byte podId = this.getPodId(messageType, channelConfig.output); + final PodIdentifier pi = new PodIdentifier(messageType, podId, outgoing); + // initialize podData + PodData pd = this.getPodData(pi); + if (outgoing) { + int outputIdx = getOutputIndex(channelConfig.output, analog); + PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd; + // we have to track value state for all outgoing channels to ensure we have valid values for all + // channels in use before we send a message to the C.M.I. otherwise it could trigger some strange things + // on TA side... + boolean set = false; + if (analog) { + TACmiChannelConfigurationAnalog ca = (TACmiChannelConfigurationAnalog) channelConfig; + Double initialValue = ca.initialValue; + if (initialValue != null) { + final TACmiMeasureType measureType = TACmiMeasureType.values()[ca.type]; + final double val = initialValue.doubleValue() * measureType.getOffset(); + @Nullable + Message message = pd.message; + if (message != null) { + // shouldn't happen, just in case... + message.setValue(outputIdx, (short) val, measureType.ordinal()); + set = true; + } + } + } else { + // digital... + TACmiChannelConfigurationDigital ca = (TACmiChannelConfigurationDigital) channelConfig; + Boolean initialValue = ca.initialValue; + if (initialValue != null) { + @Nullable + DigitalMessage message = (DigitalMessage) pd.message; + if (message != null) { + // shouldn't happen, just in case... + message.setPortState(outputIdx, initialValue); + set = true; + } + } + } + podDataOutgoing.channeUIDs[outputIdx] = chann.getUID(); + podDataOutgoing.initialized[outputIdx] = set; + } + } + + final Bridge br = getBridge(); + final TACmiCoEBridgeHandler bridge = br == null ? null : (TACmiCoEBridgeHandler) br.getHandler(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No Bridge configured!"); + return; + } + bridge.registerCMI(this); + this.bridge = bridge; + + // we set it to UNKNOWN. Will be set to ONLIN~E as soon as we start receiving + // data or to OFFLINE when no data is received within 900 seconds. + updateStatus(ThingStatus.UNKNOWN); + } + + private PodData getPodData(final PodIdentifier pi) { + PodData pd = this.podDatas.get(pi); + if (pd == null) { + if (pi.outgoing) { + pd = new PodDataOutgoing(pi, (byte) this.node); + } else { + pd = new PodData(pi, (byte) this.node); + } + this.podDatas.put(pi, pd); + } + return pd; + } + + private byte getPodId(final MessageType messageType, final int output) { + assert output >= 1 && output <= 32; // range 1-32 + // pod ID's: 0 & 9 for digital states, 1-8 for analog values + boolean analog = messageType == MessageType.ANALOG; + int outputIdx = getOutputIndex(output, analog); + if (messageType == MessageType.ANALOG) { + return (byte) (outputIdx + 1); + } + return (byte) (outputIdx == 0 ? 0 : 9); + } + + /** + * calculates output index position within the POD. + * TA output index starts with 1, our arrays starts at 0. We also have to keep the pod size in mind... + * + * @param output + * @param analog + * @return + */ + private int getOutputIndex(int output, boolean analog) { + int outputIdx = output - 1; + if (analog) { + outputIdx %= 4; + } else { + outputIdx %= 16; + } + return outputIdx; + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + final TACmiChannelConfiguration channelConfig = this.channelConfigByUID.get(channelUID); + if (channelConfig == null) { + logger.debug("Recived unhandled command '{}' for unknown Channel {} ", command, channelUID); + return; + } + final Channel channel = thing.getChannel(channelUID); + if (channel == null) { + return; + } + + if (command instanceof RefreshType) { + // we try to find the last known state from cache and return it. + MessageType mt; + if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_IN_UID.equals(channel.getChannelTypeUID()))) { + mt = MessageType.DIGITAL; + } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_IN_UID.equals(channel.getChannelTypeUID()))) { + mt = MessageType.ANALOG; + } else { + logger.debug("Recived unhandled command '{}' on unknown Channel type {} ", command, channelUID); + return; + } + final byte podId = getPodId(mt, channelConfig.output); + PodData pd = getPodData(new PodIdentifier(mt, podId, true)); + @Nullable + Message message = pd.message; + if (message == null) { + // no data received yet from the C.M.I. and persistence might be disabled.. + return; + } + if (mt == MessageType.ANALOG) { + final AnalogValue value = ((AnalogMessage) message).getAnalogValue(channelConfig.output); + updateState(channel.getUID(), new DecimalType(value.value)); + } else { + final boolean state = ((DigitalMessage) message).getPortState(channelConfig.output); + updateState(channel.getUID(), state ? OnOffType.ON : OnOffType.OFF); + } + return; + } + boolean analog; + MessageType mt; + if ((TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_OUT_UID.equals(channel.getChannelTypeUID()))) { + mt = MessageType.DIGITAL; + analog = false; + } else if ((TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_OUT_UID.equals(channel.getChannelTypeUID()))) { + mt = MessageType.ANALOG; + analog = true; + } else { + logger.debug("Recived unhandled command '{}' on Channel {} ", command, channelUID); + return; + } + + final byte podId = getPodId(mt, channelConfig.output); + PodDataOutgoing podDataOutgoing = (PodDataOutgoing) getPodData(new PodIdentifier(mt, podId, true)); + @Nullable + Message message = podDataOutgoing.message; + if (message == null) { + logger.error("Internal error - BUG - no outgoing message for command '{}' on Channel {} ", command, + channelUID); + return; + } + int outputIdx = getOutputIndex(channelConfig.output, analog); + boolean modified; + if (analog) { + final TACmiMeasureType measureType = TACmiMeasureType + .values()[((TACmiChannelConfigurationAnalog) channelConfig).type]; + final DecimalType dt = (DecimalType) command; + final double val = dt.doubleValue() * measureType.getOffset(); + modified = message.setValue(outputIdx, (short) val, measureType.ordinal()); + } else { + final boolean state = OnOffType.ON.equals(command) ? true : false; + modified = ((DigitalMessage) message).setPortState(outputIdx, state); + } + podDataOutgoing.initialized[outputIdx] = true; + if (modified) { + try { + @Nullable + final TACmiCoEBridgeHandler br = this.bridge; + @Nullable + final InetAddress cmia = this.cmiAddress; + if (br != null && cmia != null && podDataOutgoing.isAllValuesInitialized()) { + br.sendData(message.getRaw(), cmia); + podDataOutgoing.lastSent = System.currentTimeMillis(); + } + // we also update the local state after we successfully sent out the command + // there is no feedback from the C.M.I. so we only could assume the message has been received when we + // were able to send it... + updateState(channel.getUID(), (State) command); + } catch (final IOException e) { + logger.warn("Error sending message: {}: {}", e.getClass().getName(), e.getMessage()); + } + } + } + + @Override + public void dispose() { + final TACmiCoEBridgeHandler br = this.bridge; + if (br != null) { + br.unregisterCMI(this); + } + super.dispose(); + } + + public boolean isFor(final InetAddress remoteAddress, final int node) { + @Nullable + final InetAddress cmia = this.cmiAddress; + if (cmia == null) { + return false; + } + return this.node == node && cmia.equals(remoteAddress); + } + + public void handleCoE(final Message message) { + final ChannelTypeUID channelType = message.getType() == MessageType.DIGITAL + ? TACmiBindingConstants.CHANNEL_TYPE_COE_DIGITAL_IN_UID + : TACmiBindingConstants.CHANNEL_TYPE_COE_ANALOG_IN_UID; + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + this.lastMessageRecvTS = System.currentTimeMillis(); + for (final Channel channel : thing.getChannels()) { + if (!(channelType.equals(channel.getChannelTypeUID()))) { + continue; + } + final int output = ((Number) channel.getConfiguration().get(TACmiBindingConstants.CHANNEL_CONFIG_OUTPUT)) + .intValue(); + if (!message.hasPortnumber(output)) { + continue; + } + + if (message.getType() == MessageType.ANALOG) { + final AnalogValue value = ((AnalogMessage) message).getAnalogValue(output); + updateState(channel.getUID(), new DecimalType(value.value)); + } else { + final boolean state = ((DigitalMessage) message).getPortState(output); + updateState(channel.getUID(), state ? OnOffType.ON : OnOffType.OFF); + } + } + } + + public void checkForTimeout() { + final long refTs = System.currentTimeMillis(); + if (refTs - this.lastMessageRecvTS > 900000 && getThing().getStatus() != ThingStatus.OFFLINE) { + // no data received for 900 seconds - set thing status to offline.. + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "No update from C.M.I. for 15 min"); + } + for (final PodData pd : this.podDatas.values()) { + if (pd == null || !(pd instanceof PodDataOutgoing)) { + continue; + } + PodDataOutgoing podDataOutgoing = (PodDataOutgoing) pd; + @Nullable + Message message = pd.message; + if (message != null && refTs - podDataOutgoing.lastSent > 300000) { + // re-send every 300 seconds... + @Nullable + final InetAddress cmia = this.cmiAddress; + if (podDataOutgoing.isAllValuesInitialized()) { + try { + @Nullable + final TACmiCoEBridgeHandler br = this.bridge; + if (br != null && cmia != null) { + br.sendData(message.getRaw(), cmia); + podDataOutgoing.lastSent = System.currentTimeMillis(); + } + } catch (final IOException e) { + logger.warn("Error sending message to C.M.I.: {}: {}", e.getClass().getName(), e.getMessage()); + } + } else { + // pod is not entirely initialized - log warn for user but also set lastSent to prevent flooding of + // logs... + if (cmia != null) { + logger.warn("Sending data to {} {}.{} is blocked as we don't have valid values for channels {}", + cmia.getHostAddress(), this.node, podDataOutgoing.podId, + podDataOutgoing.getUninitializedChannelNames()); + } + podDataOutgoing.lastSent = System.currentTimeMillis(); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogMessage.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogMessage.java new file mode 100644 index 0000000000000..68d2654a36d60 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogMessage.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.message; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Format of analog messages is as follows: + * 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + * 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + * canNode 1|2|3|4 1.lower 1.upper 2.lower 2.upper 3.lower 3.upper 4.lower 4.upper 1.type 2.type 3.type 4.type + * + * possible values for type according to the documentation are 1 to 21. + * + * The documentation says for the types: + * + * 1: Degree Celsius + * 2: Watts per square meter + * 3: liters per hour + * 4: seconds + * 5: minutes + * 6: liters per pulse + * 7: Kelvin + * 8: Percent + * 9: Kilowatt + * 10: Megawatthours + * 11: Kilowatthours + * 12: Volt + * 13: Milliampere + * 14: hours + * 15: days + * 16: pulses + * 17: Kiloohm + * 18: Kilometers per hour + * 19: Hertz + * 20: liters per minute + * 21: bar + * + * However, reality shows that the documentation is partly not accurate. An UVR1611 device uses: + * + * 1: Degree Celsius + * 4: Seconds + * 10: Kilowatt + * 11: Megawatthours + * 12: Kilowatthours + * + * so we don't rely on the documentation. + * + * This class can be used to decode the analog values received in a message and + * also to create a new AnalogMessage used to send analog values to an analog + * CAN Input port. Creation of new message is not implemented so far. + * + * @author Timo Wendt - Initial contribution + * @author Wolfgang Klimt - improvements + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public final class AnalogMessage extends Message { + + /** + * Used to parse the data received from the CMI. + * + * @param raw + */ + public AnalogMessage(byte[] raw) { + super(raw); + } + + /** + * Create a new message to be sent to the CMI. It is only supported to use + * the first port for each podNumber. + */ + public AnalogMessage(byte canNode, byte podNumber) { + super(canNode, podNumber); + } + + /** + * Get the value for the specified port number. + * + * @param portNumber + * @return + */ + public AnalogValue getAnalogValue(int portNumber) { + // Get the internal index for portNumber within the message + int idx = (portNumber - 1) % 4; + AnalogValue value = new AnalogValue(this.getValue(idx), getMeasureType(idx)); + return value; + } + + /** + * Check if message contains a value for the specified port number. It + * doesn't matter though if the port has a value of 0. + * + * @param portNumber + * @return + */ + @Override + public boolean hasPortnumber(int portNumber) { + return (portNumber - 1) / 4 == podNumber - 1; + } + + @Override + public MessageType getType() { + return MessageType.ANALOG; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogValue.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogValue.java new file mode 100644 index 0000000000000..c40ec4bc06cd0 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/AnalogValue.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.message; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.tacmi.internal.TACmiMeasureType; + +/** + * This class handles analog values as used in the analog message. + * + * @author Timo Wendt - Initial contribution + * @author Wolfgang Klimt + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public final class AnalogValue { + public double value; + public TACmiMeasureType measureType; + + /** + * Create new AnalogValue with specified value and type + */ + public AnalogValue(int rawValue, int type) { + measureType = TACmiMeasureType.fromInt(type); + value = ((double) rawValue) / measureType.getOffset(); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/DigitalMessage.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/DigitalMessage.java new file mode 100644 index 0000000000000..d7d5d5dbaa535 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/DigitalMessage.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.message; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class can be used to decode the digital values received in a messag and + * also to create a new DigitalMessage used to send ON/OFF to an digital CAN + * Input port + * + * @author Timo Wendt - Initial contribution + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public final class DigitalMessage extends Message { + + public DigitalMessage(byte[] raw) { + super(raw); + } + + /** + * Create a new message to be sent to the CMI. It is only supported to use the + * first port for each CAN node. This is due to the fact that all digital port + * for the specific CAN node are send within a single message. + */ + public DigitalMessage(byte canNode, byte podNr) { + super(canNode, podNr); + } + + /** + * Get the state of the specified port number. + * + * @param portNumber + * @return + */ + public boolean getPortState(int portNumber) { + return getBit(getValue(0), (portNumber - 1) % 16); + } + + /** + * Set the state of the specified port number. + * + * @param portNumber + * @param value + * @return + */ + public boolean setPortState(int portNumber, boolean value) { + short val = getValue(0); + int bit = (1 << portNumber); + if (value) { + val |= bit; + } else { + val &= ~bit; + } + return setValue(0, val, 0); + } + + /** + * Read the specified bit from the short value holding the states of all 16 + * ports. + * + * @param portBits + * @param portBit + * @return + */ + private boolean getBit(int portBits, int portBit) { + int result = (portBits >> portBit) & 0x1; + return result == 1 ? true : false; + } + + /** + * Check if message contains a value for the specified port number. portNumber + * Digital messages are in POD 0 for 1-16 and POD 9 for 17-32 + * + * @param portNumber - the portNumber in Range 1-32 + * @return + */ + @Override + public boolean hasPortnumber(int portNumber) { + if (podNumber == 0 && portNumber <= 16) { + return true; + } + if (podNumber == 9 && portNumber >= 17) { + return true; + } + return false; + } + + @Override + public MessageType getType() { + return MessageType.DIGITAL; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/Message.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/Message.java new file mode 100644 index 0000000000000..b88f61684e67f --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/Message.java @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.message; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base message class handling generic functions. + * + * @author Timo Wendt - Initial contribution + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public abstract class Message { + + protected final Logger logger = LoggerFactory.getLogger(Message.class); + + /** + * ByteBuffer that stores the content of the message. + */ + private ByteBuffer buffer; + + /** + * CAN Node number used in the message + */ + public byte canNode; + + /** + * POD number used in the message + */ + public byte podNumber; + + /** + * Initialize from the bytes of a received message + * + * @param raw + */ + public Message(byte[] raw) { + this.buffer = ByteBuffer.wrap(raw); + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + this.canNode = buffer.get(0); + this.podNumber = buffer.get(1); + } + + /** + * Used to create a new message with the specified CAN node and POD number + * + * @param canNode + * @param podNumber + */ + public Message(int canNode, int podNumber) { + this.buffer = ByteBuffer.allocate(14); + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + setCanNode(canNode); + setPodNumber(podNumber); + } + + public abstract MessageType getType(); + + public abstract boolean hasPortnumber(int portNumber); + + /** + * Get the byte array. This can be sent to the CMI. + * + * @return raw + */ + public byte[] getRaw() { + return buffer.array(); + } + + /** + * Set the CAN node number for this message + * + * @param canNode + */ + public void setCanNode(int canNode) { + buffer.put(0, (byte) (canNode & 0xff)); + } + + /** + * Set the POD number for this message + * + * @param podNumber + */ + public void setPodNumber(int podNumber) { + buffer.put(1, (byte) (podNumber & 0xf)); + } + + /** + * Set the value at th specified index within the message and the defined + * measure type. The measure type is only used in analog messages. Digital + * messages always use 0 for the measure types. + * + * @param idx + * @param value + * @param measureType + * @return true when value was modified + */ + public boolean setValue(int idx, short value, int measureType) { + boolean modified = false; + int idxValue = idx * 2 + 2; + if (buffer.getShort(idxValue) != value) { + buffer.putShort(idxValue, value); + modified = true; + } + byte mtv = (byte) (measureType & 0xf); + if (buffer.get(idx + 10) != mtv) { + buffer.put(idx + 10, mtv); + modified = true; + } + return modified; + } + + /** + * Get the value at the specified index within the message. The value will + * be converted from thr signed short to an unsigned int. + * + * @param idx + * @return + */ + public short getValue(int idx) { + return (buffer.getShort(idx * 2 + 2)); + } + + /** + * Get the measure type for the specified index within the message. + * + * @param idx + * @return + */ + public int getMeasureType(int idx) { + return (buffer.get(idx + 10)) & 0xffff; + } + + @Override + public String toString() { + return ("CAN: " + this.canNode + " POD: " + this.podNumber + " Value1: " + getValue(0) + " Value2: " + + getValue(1) + " Value3: " + getValue(2) + " Value4: " + getValue(3) + " MeasureType1 " + + getMeasureType(0) + " MeasureType2 " + getMeasureType(1) + " MeasureType3 " + getMeasureType(2) + + " MeasureType4 " + getMeasureType(3)); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/MessageType.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/MessageType.java new file mode 100644 index 0000000000000..7e708462ebe1e --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/message/MessageType.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.message; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This enumeration represents the different message types provided by the C.M.I COE protocol. + * + * @author Timo Wendt - Initial contribution + * @author Christian Niessner - Ported to OpenHAB2 + */ +@NonNullByDefault +public enum MessageType { + ANALOG, + DIGITAL +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java new file mode 100644 index 0000000000000..ba4c2a6270993 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageEntry.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.schema; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.types.State; + +/** + * The {@link ApiPageEntry} class contains mapping information for an entry of + * the API page. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class ApiPageEntry { + + static enum Type { + READ_ONLY_SWITCH(true), + READ_ONLY_NUMERIC(true), + NUMERIC_FORM(false), + SWITCH_BUTTON(false), + SWITCH_FORM(false), + READ_ONLY_STATE(true), + STATE_FORM(false); + + public final boolean readOnly; + + private Type(boolean readOnly) { + this.readOnly = readOnly; + } + } + + /** + * type of this entry + */ + public final Type type; + + /** + * The channel for this entry + */ + public final Channel channel; + + /** + * internal address for this channel + */ + public final @Nullable String address; + + /** + * data for handle 'changerx2' form fields + */ + public final @Nullable ChangerX2Entry changerX2Entry; + + /** + * The last known state for this item... + */ + private State lastState; + + protected ApiPageEntry(final Type type, final Channel channel, @Nullable final String address, + @Nullable ChangerX2Entry changerX2Entry, State lastState) { + this.type = type; + this.channel = channel; + this.address = address; + this.changerX2Entry = changerX2Entry; + this.lastState = lastState; + } + + public void setLastState(State lastState) { + this.lastState = lastState; + } + + public State getLastState() { + return lastState; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java new file mode 100644 index 0000000000000..c6d9a7a151218 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ApiPageParser.java @@ -0,0 +1,494 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.schema; + +import java.math.BigDecimal; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.attoparser.ParseException; +import org.attoparser.simple.AbstractSimpleMarkupHandler; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder; +import org.eclipse.smarthome.core.thing.type.ChannelType; +import org.eclipse.smarthome.core.thing.type.ChannelTypeBuilder; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.StateDescriptionFragmentBuilder; +import org.eclipse.smarthome.core.types.StateOption; +import org.openhab.binding.tacmi.internal.TACmiBindingConstants; +import org.openhab.binding.tacmi.internal.TACmiChannelTypeProvider; +import org.openhab.binding.tacmi.internal.schema.ApiPageEntry.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ApiPageParser} class parses the 'API' schema page from the CMI and + * maps it to our channels + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class ApiPageParser extends AbstractSimpleMarkupHandler { + + private final Logger logger = LoggerFactory.getLogger(ApiPageParser.class); + + static enum ParserState { + INIT, + DATA_ENTRY + } + + static enum FieldType { + UNKNOWN, + READ_ONLY, + FORM_VALUE, + BUTTON, + IGNORE + } + + static enum ButtonValue { + UNKNOWN, + ON, + OFF + } + + private ParserState parserState = ParserState.INIT; + private TACmiSchemaHandler taCmiSchemaHandler; + private TACmiChannelTypeProvider channelTypeProvider; + private boolean configChanged = false; + private FieldType fieldType = FieldType.UNKNOWN; + private @Nullable String id; + private @Nullable String address; + private @Nullable StringBuilder value; + private ButtonValue buttonValue = ButtonValue.UNKNOWN; + private Map entries; + private Set seenNames = new HashSet<>(); + private List channels = new ArrayList<>(); + + public ApiPageParser(TACmiSchemaHandler taCmiSchemaHandler, Map entries, + TACmiChannelTypeProvider channelTypeProvider) { + super(); + this.taCmiSchemaHandler = taCmiSchemaHandler; + this.entries = entries; + this.channelTypeProvider = channelTypeProvider; + } + + @Override + public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException { + this.parserState = ParserState.INIT; + this.seenNames.clear(); + this.channels.clear(); + } + + @Override + public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col) + throws ParseException { + if (this.parserState != ParserState.INIT) { + logger.debug("Parserstate == Init expected, but is {}", this.parserState); + } + } + + @Override + @NonNullByDefault({}) + public void handleStandaloneElement(final @Nullable String elementName, + final @Nullable Map attributes, final boolean minimized, final int line, final int col) + throws ParseException { + + logger.debug("Unexpected StandaloneElement in {}:{}: {} [{}]", line, col, elementName, attributes); + } + + @Override + @NonNullByDefault({}) + public void handleOpenElement(final @Nullable String elementName, final @Nullable Map attributes, + final int line, final int col) throws ParseException { + + if (this.parserState == ParserState.INIT && "div".equals(elementName)) { + this.parserState = ParserState.DATA_ENTRY; + String classFlags; + if (attributes == null) { + classFlags = null; + this.id = null; + this.address = null; + } else { + this.id = attributes.get("id"); + this.address = attributes.get("adresse"); + classFlags = attributes.get("class"); + } + this.fieldType = FieldType.READ_ONLY; + this.value = new StringBuilder(); + this.buttonValue = ButtonValue.UNKNOWN; + if (classFlags != null && StringUtil.isNotBlank(classFlags)) { + String[] classFlagList = classFlags.split("[ \n\r]"); + for (String classFlag : classFlagList) { + if ("changex2".equals(classFlag)) { + this.fieldType = FieldType.FORM_VALUE; + } else if ("buttonx2".equals(classFlag) || "taster".equals(classFlag)) { + this.fieldType = FieldType.BUTTON; + } else if ("visible0".equals(classFlag)) { + this.buttonValue = ButtonValue.OFF; + } else if ("visible1".equals(classFlag)) { + this.buttonValue = ButtonValue.ON; + } else if ("durchsichtig".equals(classFlag)) { // link + this.fieldType = FieldType.IGNORE; + } else if ("bord".equals(classFlag)) { // special button style - not of our interest... + } else { + logger.debug("Unhanndled class in {}:{}:{}: '{}' ", id, line, col, classFlag); + } + } + } + } else if (this.parserState == ParserState.DATA_ENTRY && this.fieldType == FieldType.BUTTON + && "span".equals(elementName)) { + // ignored... + } else { + logger.debug("Unexpected OpenElement in {}:{}: {} [{}]", line, col, elementName, attributes); + } + } + + @Override + public void handleCloseElement(final @Nullable String elementName, final int line, final int col) + throws ParseException { + if (this.parserState == ParserState.DATA_ENTRY && "div".equals(elementName)) { + this.parserState = ParserState.INIT; + StringBuilder sb = this.value; + this.value = null; + if (sb != null) { + while (sb.length() > 0 && sb.charAt(0) == ' ') { + sb = sb.delete(0, 0); + } + if (this.fieldType == FieldType.READ_ONLY || this.fieldType == FieldType.FORM_VALUE) { + int lids = sb.lastIndexOf(":"); + int fsp = sb.indexOf(" "); + if (fsp < 0 || lids < 0 || fsp > lids) { + logger.debug("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType, + sb); + } else { + String shortName = sb.substring(0, fsp).trim(); + String description = sb.substring(fsp + 1, lids).trim(); + String value = sb.substring(lids + 1).trim(); + getApiPageEntry(id, line, col, shortName, description, value); + } + } else if (this.fieldType == FieldType.BUTTON) { + String sbt = sb.toString().trim().replaceAll("[\r\n ]+", " "); + int fsp = sbt.indexOf(" "); + + if (fsp < 0) { + logger.debug("Invalid format for setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType, + sbt); + } else { + String shortName = sbt.substring(0, fsp).trim(); + String description = sbt.substring(fsp + 1).trim(); + getApiPageEntry(id, line, col, shortName, description, this.buttonValue); + } + } else if (this.fieldType == FieldType.IGNORE) { + // ignore + } else { + logger.debug("Unhandled setting {}:{}:{} [{}] : {}", id, line, col, this.fieldType, sb); + } + } + } else if (this.parserState == ParserState.DATA_ENTRY && this.fieldType == FieldType.BUTTON + && "span".equals(elementName)) { + // ignored... + } else { + logger.debug("Unexpected CloseElement in {}:{}: {}", line, col, elementName); + } + } + + @Override + public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col) + throws ParseException { + logger.debug("Unexpected AutoCloseElement in {}:{}: {}", line, col, elementName); + } + + @Override + public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col) + throws ParseException { + logger.debug("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col, elementName); + } + + @Override + public void handleDocType(final @Nullable String elementName, final @Nullable String publicId, + final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col) + throws ParseException { + logger.debug("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId, + internalSubset); + } + + @Override + public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line, + final int col) throws ParseException { + logger.debug("Unexpected comment in {}:{}: {}", line, col, new String(buffer, offset, len)); + } + + @Override + public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line, + final int col) throws ParseException { + logger.debug("Unexpected CDATA in {}:{}: {}", line, col, new String(buffer, offset, len)); + } + + @Override + public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line, + final int col) throws ParseException { + + if (buffer == null) { + return; + } + + if (this.parserState == ParserState.DATA_ENTRY) { + // we append it to our current value + StringBuilder sb = this.value; + if (sb != null) { + sb.append(buffer, offset, len); + } + } else if (this.parserState == ParserState.INIT && ((len == 1 && buffer[offset] == '\n') + || (len == 2 && buffer[offset] == '\r' && buffer[offset + 1] == '\n'))) { + // single newline - ignore/drop it... + } else { + String msg = new String(buffer, offset, len).replace("\n", "\\n").replace("\r", "\\r"); + logger.debug("Unexpected Text {}:{}: ParserState: {} ({}) `{}`", line, col, parserState, len, msg); + } + } + + @Override + public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding, + final @Nullable String standalone, final int line, final int col) throws ParseException { + logger.debug("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone); + } + + @Override + public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content, + final int line, final int col) throws ParseException { + logger.debug("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content); + } + + private void getApiPageEntry(@Nullable String id2, int line, int col, String shortName, String description, + Object value) { + if (logger.isDebugEnabled()) { + logger.debug("Found parameter {}:{}:{} [{}] : {} \"{}\" = {}", id, line, col, this.fieldType, shortName, + description, value); + } + if (!this.seenNames.add(shortName)) { + logger.warn("Found duplicate parameter '{}' in {}:{}:{} [{}] : {} \"{}\" = {}", shortName, id, line, col, + this.fieldType, shortName, description, value); + return; + } + + if (value instanceof String && ((String) value).contains("can_busy")) { + return; // special state to indicate value currently cannot be retrieved.. + } + ApiPageEntry.Type type; + State state; + String channelType; + ChannelTypeUID ctuid; + switch (this.fieldType) { + case BUTTON: + type = Type.SWITCH_BUTTON; + state = this.buttonValue == ButtonValue.ON ? OnOffType.ON : OnOffType.OFF; + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID; + channelType = "Switch"; + break; + case READ_ONLY: + case FORM_VALUE: + String vs = (String) value; + boolean isOn = "ON".equals(vs) || "EIN".equals(vs); // C.M.I. mixes up languages... + if (isOn || "OFF".equals(vs) || "AUS".equals(vs)) { + channelType = "Switch"; + state = isOn ? OnOffType.ON : OnOffType.OFF; + if (this.fieldType == FieldType.READ_ONLY || this.address == null) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RO_UID; + type = Type.READ_ONLY_SWITCH; + } else { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_SWITCH_RW_UID; + type = Type.SWITCH_FORM; + } + } else { + try { + // check if we have a numeric value (either with or without unit) + String[] valParts = vs.split(" "); + // It seems for some wired cases the C.M.I. uses different decimal separators for + // different device types. It seems all 'new' X2-Devices use a dot as separator, + // for the older pre-X2 devices (i.e. the UVR 1611) we get a comma. So we + // we replace all ',' with '.' to check if it's a valid number... + String val = valParts[0].replace(',', '.'); + BigDecimal bd = new BigDecimal(val); + if (valParts.length == 2) { + if ("°C".equals(valParts[1])) { + channelType = "Number:Temperature"; + state = new QuantityType<>(bd, SIUnits.CELSIUS); + } else if ("%".equals(valParts[1])) { + channelType = "Number:Percent"; + state = new QuantityType<>(bd, SmartHomeUnits.PERCENT); + } else if ("Imp".equals(valParts[1])) { + // impulses - no idea how to map this to something useful here? + channelType = "Number"; + state = new DecimalType(bd); + } else if ("V".equals(valParts[1])) { + channelType = "Number:Voltage"; + state = new QuantityType<>(bd, SmartHomeUnits.VOLT); + } else if ("A".equals(valParts[1])) { + channelType = "Number:Current"; + state = new QuantityType<>(bd, SmartHomeUnits.AMPERE); + } else if ("Hz".equals(valParts[1])) { + channelType = "Number:Frequency"; + state = new QuantityType<>(bd, SmartHomeUnits.HERTZ); + } else if ("kW".equals(valParts[1])) { + channelType = "Number:Power"; + bd = bd.multiply(new BigDecimal(1000)); + state = new QuantityType<>(bd, SmartHomeUnits.WATT); + } else if ("kWh".equals(valParts[1])) { + channelType = "Number:Power"; + bd = bd.multiply(new BigDecimal(1000)); + state = new QuantityType<>(bd, SmartHomeUnits.KILOWATT_HOUR); + } else if ("l/h".equals(valParts[1])) { + channelType = "Number:Volume"; + bd = bd.divide(new BigDecimal(60)); + state = new QuantityType<>(bd, SmartHomeUnits.LITRE_PER_MINUTE); + } else { + channelType = "Number"; + state = new DecimalType(bd); + logger.debug("Unhandled UoM for channel {} of type {} for '{}': {}", shortName, + channelType, description, valParts[1]); + } + } else { + channelType = "Number"; + state = new DecimalType(bd); + } + if (this.fieldType == FieldType.READ_ONLY || this.address == null) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID; + type = Type.READ_ONLY_NUMERIC; + } else { + ctuid = null; + type = Type.NUMERIC_FORM; + } + } catch (NumberFormatException nfe) { + // not a number... + channelType = "String"; + if (this.fieldType == FieldType.READ_ONLY || this.address == null) { + ctuid = TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID; + type = Type.READ_ONLY_STATE; + } else { + ctuid = null; + type = Type.STATE_FORM; + } + state = new StringType(vs); + } + } + break; + case UNKNOWN: + case IGNORE: + return; + default: + // should't happen but we have to add default for the compiler... + return; + } + ApiPageEntry e = this.entries.get(shortName); + if (e == null || e.type != type || !channelType.equals(e.channel.getAcceptedItemType())) { + @Nullable + Channel channel = this.taCmiSchemaHandler.getThing().getChannel(shortName); + @Nullable + ChangerX2Entry cx2e = null; + if (this.fieldType == FieldType.FORM_VALUE) { + try { + URI uri = this.taCmiSchemaHandler.buildUri("INCLUDE/changerx2.cgi?sadrx2=" + address); + final ChangerX2Parser pp = this.taCmiSchemaHandler.parsePage(uri, new ChangerX2Parser(shortName)); + cx2e = pp.getParsedEntry(); + } catch (final ParseException | RuntimeException ex) { + logger.warn("Error parsing API Scheme: {} ", ex.getMessage(), ex); + } catch (final TimeoutException | InterruptedException | ExecutionException ex) { + logger.warn("Error loading API Scheme: {} ", ex.getMessage()); + } + } + if (channel == null) { + logger.debug("Creating / updating channel {} of type {} for '{}'", shortName, channelType, description); + this.configChanged = true; + ChannelUID channelUID = new ChannelUID(this.taCmiSchemaHandler.getThing().getUID(), shortName); + ChannelBuilder channelBuilder = ChannelBuilder.create(channelUID, channelType); + channelBuilder.withLabel(description); + if (ctuid != null) { + channelBuilder.withType(ctuid); + } else if (cx2e != null) { + StateDescriptionFragmentBuilder sdb = StateDescriptionFragmentBuilder.create() + .withReadOnly(type.readOnly); + String itemType; + switch (cx2e.optionType) { + case NUMBER: + itemType = "Number"; + String min = cx2e.options.get(ChangerX2Entry.NUMBER_MIN); + if (min != null && !min.trim().isEmpty()) { + sdb.withMinimum(new BigDecimal(min)); + } + String max = cx2e.options.get(ChangerX2Entry.NUMBER_MAX); + if (max != null && !max.trim().isEmpty()) { + sdb.withMaximum(new BigDecimal(max)); + } + String step = cx2e.options.get(ChangerX2Entry.NUMBER_STEP); + if (step != null && !step.trim().isEmpty()) { + sdb.withStep(new BigDecimal(step)); + } + break; + case SELECT: + itemType = "String"; + for (Entry entry : cx2e.options.entrySet()) { + String val = entry.getValue(); + if (val != null) { + sdb.withOption(new StateOption(val, entry.getKey())); + } + } + break; + default: + throw new IllegalStateException(); + } + ChannelType ct = ChannelTypeBuilder + .state(new ChannelTypeUID(TACmiBindingConstants.BINDING_ID, shortName), shortName, itemType) + .withDescription("Auto-created for " + shortName) + .withStateDescription(sdb.build().toStateDescription()).build(); + channelTypeProvider.addChannelType(ct); + channelBuilder.withType(ct.getUID()); + } else { + logger.warn("Error configurating channel for {}: channeltype cannot be determined!", shortName); + } + channel = channelBuilder.build(); // add configuration property... + } + this.configChanged = true; + e = new ApiPageEntry(type, channel, address, cx2e, state); + this.entries.put(shortName, e); + } + this.channels.add(e.channel); + e.setLastState(state); + this.taCmiSchemaHandler.updateState(e.channel.getUID(), state); + } + + protected boolean isConfigChanged() { + return this.configChanged; + } + + protected List getChannels() { + return channels; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java new file mode 100644 index 0000000000000..216d86d51e63e --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Entry.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.schema; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ChangerX2Entry} class contains mapping information for a changerX2 entry of + * the API page element + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class ChangerX2Entry { + + public static final String NUMBER_MIN = "min"; + public static final String NUMBER_MAX = "max"; + public static final String NUMBER_STEP = "step"; + + static enum OptionType { + NUMBER, + SELECT, + } + + /** + * field name of the address + */ + public final String addressFieldName; + + /** + * The address these options are for + */ + public final String address; + + /** + * option type + */ + public final OptionType optionType; + + /** + * field name of the option value + */ + public final String optionFieldName; + + /** + * the valid options + */ + public final Map options; + + public ChangerX2Entry(String addressFieldName, String address, String optionFieldName, OptionType optionType, + Map options) { + this.addressFieldName = addressFieldName; + this.address = address; + this.optionFieldName = optionFieldName; + this.optionType = optionType; + this.options = options; + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java new file mode 100644 index 0000000000000..3270c1e5d22fb --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/ChangerX2Parser.java @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.schema; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.attoparser.ParseException; +import org.attoparser.simple.AbstractSimpleMarkupHandler; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.tacmi.internal.schema.ChangerX2Entry.OptionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ApiPageParser} class parses the 'changerx2' page from the CMI and + * maps it to the results + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class ChangerX2Parser extends AbstractSimpleMarkupHandler { + + private final Logger logger = LoggerFactory.getLogger(ChangerX2Parser.class); + + static enum ParserState { + INIT, + INPUT, + INPUT_DATA, + SELECT, + SELECT_OPTION, + UNKNOWN + } + + private final String channelName; + private @Nullable String curOptionId; + private ParserState parserState = ParserState.INIT; + private @Nullable String address; + private @Nullable String addressFieldName; + private @Nullable String optionFieldName; + private @Nullable OptionType optionType; + private @Nullable StringBuilder curOptionValue; + private Map options; + + public ChangerX2Parser(String channelName) { + super(); + this.options = new LinkedHashMap<>(); + this.channelName = channelName; + } + + @Override + public void handleDocumentStart(final long startTimeNanos, final int line, final int col) throws ParseException { + this.parserState = ParserState.INIT; + this.options.clear(); + } + + @Override + public void handleDocumentEnd(final long endTimeNanos, final long totalTimeNanos, final int line, final int col) + throws ParseException { + if (this.parserState != ParserState.INIT) { + logger.debug("Parserstate == Init expected, but is {}", this.parserState); + } + } + + @Override + @NonNullByDefault({}) + public void handleStandaloneElement(final String elementName, final Map attributes, + final boolean minimized, final int line, final int col) throws ParseException { + + logger.debug("Error parsing options for {}: Unexpected StandaloneElement in {}{}: {} [{}]", channelName, line, + col, elementName, attributes); + } + + @Override + @NonNullByDefault({}) + public void handleOpenElement(final String elementName, final Map attributes, final int line, + final int col) throws ParseException { + + String id = attributes == null ? null : attributes.get("id"); + + if (this.parserState == ParserState.INIT && "input".equals(elementName) && "changeadr".equals(id)) { + this.parserState = ParserState.INPUT; + if (attributes == null) { + this.address = null; + this.addressFieldName = null; + } else { + this.addressFieldName = attributes.get("name"); + this.address = attributes.get("value"); + } + } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT) + && "select".equals(elementName)) { + this.parserState = ParserState.SELECT; + this.optionFieldName = attributes == null ? null : attributes.get("name"); + } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT) + && "br".equals(elementName)) { + // ignored + } else if ((this.parserState == ParserState.INIT || this.parserState == ParserState.INPUT) + && "input".equals(elementName) && "changeto".equals(id)) { + this.parserState = ParserState.INPUT_DATA; + if (attributes != null) { + this.optionFieldName = attributes.get("name"); + String type = attributes.get("type"); + if ("number".equals(type)) { + this.optionType = OptionType.NUMBER; + // we transfer the limits from the input elemnt... + this.options.put(ChangerX2Entry.NUMBER_MIN, attributes.get(ChangerX2Entry.NUMBER_MIN)); + this.options.put(ChangerX2Entry.NUMBER_MAX, attributes.get(ChangerX2Entry.NUMBER_MAX)); + this.options.put(ChangerX2Entry.NUMBER_STEP, attributes.get(ChangerX2Entry.NUMBER_STEP)); + } else { + logger.warn("Error parsing options for {}: Unhandled input field in {}:{}: {}", channelName, line, + col, attributes); + } + } + } else if (this.parserState == ParserState.SELECT && "option".equals(elementName)) { + this.parserState = ParserState.SELECT_OPTION; + this.optionType = OptionType.SELECT; + this.curOptionValue = new StringBuilder(); + this.curOptionId = attributes == null ? null : attributes.get("value"); + } else { + logger.debug("Error parsing options for {}: Unexpected OpenElement in {}:{}: {} [{}]", channelName, line, + col, elementName, attributes); + } + } + + @Override + public void handleCloseElement(final @Nullable String elementName, final int line, final int col) + throws ParseException { + if (this.parserState == ParserState.INPUT && "input".equals(elementName)) { + this.parserState = ParserState.INIT; + } else if (this.parserState == ParserState.SELECT && "select".equals(elementName)) { + this.parserState = ParserState.INIT; + } else if (this.parserState == ParserState.SELECT_OPTION && "option".equals(elementName)) { + this.parserState = ParserState.SELECT; + StringBuilder sb = this.curOptionValue; + String value = sb != null && sb.length() > 0 ? sb.toString().trim() : null; + this.curOptionValue = null; + String id = this.curOptionId; + this.curOptionId = null; + if (value != null) { + if (id == null || id.trim().isEmpty()) { + logger.debug("Error parsing options for {}: Got option with empty 'value' in {}:{}: [{}]", + channelName, line, col, value); + return; + } + // we use the value as key and the id as value, as we have to map from the value to the id... + @Nullable + String prev = this.options.put(value, id); + if (prev != null && !prev.equals(value)) { + logger.debug("Error parsing options for {}: Got duplicate options in {}:{} for {}: {} and {}", + channelName, line, col, value, prev, id); + } + } + } else { + logger.debug("Error parsing options for {}: Unexpected CloseElement in {}:{}: {}", channelName, line, col, + elementName); + } + } + + @Override + public void handleAutoCloseElement(final @Nullable String elementName, final int line, final int col) + throws ParseException { + logger.debug("Unexpected AutoCloseElement in {}:{}: {}", line, col, elementName); + } + + @Override + public void handleUnmatchedCloseElement(final @Nullable String elementName, final int line, final int col) + throws ParseException { + logger.debug("Unexpected UnmatchedCloseElement in {}:{}: {}", line, col, elementName); + } + + @Override + public void handleDocType(final @Nullable String elementName, final @Nullable String publicId, + final @Nullable String systemId, final @Nullable String internalSubset, final int line, final int col) + throws ParseException { + logger.debug("Unexpected DocType in {}:{}: {}/{}/{}/{}", line, col, elementName, publicId, systemId, + internalSubset); + } + + @Override + public void handleComment(final char @Nullable [] buffer, final int offset, final int len, final int line, + final int col) throws ParseException { + logger.debug("Unexpected comment in {}:{}: {}", line, col, new String(buffer, offset, len)); + } + + @Override + public void handleCDATASection(final char @Nullable [] buffer, final int offset, final int len, final int line, + final int col) throws ParseException { + logger.debug("Unexpected CDATA in {}:{}: {}", line, col, new String(buffer, offset, len)); + } + + @Override + public void handleText(final char @Nullable [] buffer, final int offset, final int len, final int line, + final int col) throws ParseException { + + if (buffer == null) { + return; + } + + if (this.parserState == ParserState.SELECT_OPTION) { + // logger.debug("Text {}:{}: {}", line, col, new String(buffer, offset, len)); + StringBuilder sb = this.curOptionValue; + if (sb != null) { + sb.append(buffer, offset, len); + } + } else if (this.parserState == ParserState.INIT && len == 1 && buffer[offset] == '\n') { + // single newline - ignore/drop it... + } else if (this.parserState == ParserState.INPUT) { + // this is a label next to the value input field - we currently have no use for it so + // it's dropped... + } else { + logger.debug("Error parsing options for {}: Unexpected Text {}:{}: (ctx: {} len: {}) '{}' ", + this.channelName, line, col, this.parserState, len, new String(buffer, offset, len)); + } + } + + @Override + public void handleXmlDeclaration(final @Nullable String version, final @Nullable String encoding, + final @Nullable String standalone, final int line, final int col) throws ParseException { + logger.debug("Unexpected XML Declaration {}:{}: {} {} {}", line, col, version, encoding, standalone); + } + + @Override + public void handleProcessingInstruction(final @Nullable String target, final @Nullable String content, + final int line, final int col) throws ParseException { + logger.debug("Unexpected ProcessingInstruction {}:{}: {} {}", line, col, target, content); + } + + @Nullable + protected ChangerX2Entry getParsedEntry() { + String addressFieldName = this.addressFieldName; + String address = this.address; + String optionFieldName = this.optionFieldName; + OptionType optionType = this.optionType; + if (address == null || addressFieldName == null || optionType == null || optionFieldName == null) { + return null; + } + return new ChangerX2Entry(addressFieldName, address, optionFieldName, optionType, this.options); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java new file mode 100644 index 0000000000000..081eb8d39145a --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.schema; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TACmiSchemaConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiSchemaConfiguration { + + /** + * host address of the C.M.I. + */ + public String host = ""; + + /** + * Username + */ + public String username = ""; + + /** + * Password + */ + public String password = ""; + + /** + * ID of API schema page + */ + public int schemaId; + + /** + * API page poll intervall + */ + public int pollInterval; +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java new file mode 100644 index 0000000000000..2288a3b1addfa --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/schema/TACmiSchemaHandler.java @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tacmi.internal.schema; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.attoparser.ParseException; +import org.attoparser.config.ParseConfiguration; +import org.attoparser.config.ParseConfiguration.ElementBalancing; +import org.attoparser.config.ParseConfiguration.UniqueRootElementPresence; +import org.attoparser.simple.AbstractSimpleMarkupHandler; +import org.attoparser.simple.ISimpleMarkupParser; +import org.attoparser.simple.SimpleMarkupParser; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.util.B64Code; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.StringType; +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; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.builder.ThingBuilder; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.tacmi.internal.TACmiChannelTypeProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link TACmiSchemaHandler} is responsible for handling commands, which are sent + * to one of the channels. + * + * @author Christian Niessner - Initial contribution + */ +@NonNullByDefault +public class TACmiSchemaHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(TACmiSchemaHandler.class); + + private final HttpClient httpClient; + private final TACmiChannelTypeProvider channelTypeProvider; + private final Map entries = new HashMap<>(); + private boolean online; + private @Nullable String serverBase; + private @Nullable URI schemaApiPage; + private @Nullable String authHeader; + private @Nullable ScheduledFuture scheduledFuture; + private final ParseConfiguration noRestrictions; + + public TACmiSchemaHandler(final Thing thing, final HttpClient httpClient, + final TACmiChannelTypeProvider channelTypeProvider) { + super(thing); + this.httpClient = httpClient; + this.channelTypeProvider = channelTypeProvider; + + // the default configuration for the parser + this.noRestrictions = ParseConfiguration.xmlConfiguration(); + this.noRestrictions.setElementBalancing(ElementBalancing.NO_BALANCING); + this.noRestrictions.setNoUnmatchedCloseElementsRequired(false); + this.noRestrictions.setUniqueAttributesInElementRequired(false); + this.noRestrictions.setXmlWellFormedAttributeValuesRequired(false); + this.noRestrictions.setUniqueRootElementPresence(UniqueRootElementPresence.NOT_VALIDATED); + this.noRestrictions.getPrologParseConfiguration().setValidateProlog(false); + } + + @Override + public void initialize() { + final TACmiSchemaConfiguration config = getConfigAs(TACmiSchemaConfiguration.class); + + if (config.host.trim().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!"); + return; + } + if (config.username.trim().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No username configured!"); + return; + } + if (config.password.trim().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No password configured!"); + return; + } + this.online = false; + updateStatus(ThingStatus.UNKNOWN); + + this.authHeader = "Basic " + + B64Code.encode(config.username + ":" + config.password, StandardCharsets.ISO_8859_1); + + final String serverBase = "http://" + config.host + "/"; + this.serverBase = serverBase; + this.schemaApiPage = buildUri("schematic_files/" + config.schemaId + ".cgi"); + + refreshData(); + if (config.pollInterval <= 0) { + config.pollInterval = 10; + } + // we want to trigger the initial refresh 'at once' + this.scheduledFuture = scheduler.scheduleWithFixedDelay(this::refreshData, 0, config.pollInterval, + TimeUnit.SECONDS); + } + + protected URI buildUri(String path) { + return URI.create(serverBase + path); + } + + private Request prepareRequest(final URI uri) { + final Request req = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(10000, TimeUnit.MILLISECONDS); + req.header(HttpHeader.ACCEPT_LANGUAGE, "en"); // we want the on/off states in english + final String ah = this.authHeader; + if (ah != null) { + req.header(HttpHeader.AUTHORIZATION, ah); + } + return req; + } + + protected PP parsePage(URI uri, PP pp) + throws ParseException, InterruptedException, TimeoutException, ExecutionException { + final ContentResponse response = prepareRequest(uri).send(); + + String responseString = null; + String encoding = response.getEncoding(); + if (encoding == null || encoding.trim().isEmpty()) { + // the C.M.I. dosn't sometime return a valid encoding - but it defaults to UTF-8 instead of ISO... + responseString = new String(response.getContent(), StandardCharsets.UTF_8); + } else { + responseString = response.getContentAsString(); + } + + if (logger.isDebugEnabled()) { + logger.debug("Response body was: {} ", responseString); + } + + final ISimpleMarkupParser parser = new SimpleMarkupParser(this.noRestrictions); + parser.parse(responseString, pp); + return pp; + } + + private void refreshData() { + URI schemaApiPage = this.schemaApiPage; + if (schemaApiPage == null) { + return; + } + try { + final ApiPageParser pp = parsePage(schemaApiPage, + new ApiPageParser(this, entries, this.channelTypeProvider)); + + if (pp.isConfigChanged()) { + // we have to update our channels... + final List channels = pp.getChannels(); + final ThingBuilder thingBuilder = editThing(); + thingBuilder.withChannels(channels); + updateThing(thingBuilder.build()); + } + if (!this.online) { + updateStatus(ThingStatus.ONLINE); + this.online = true; + } + } catch (final InterruptedException e) { + // binding shutdown is in progress + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE); + this.online = false; + } catch (final ParseException | RuntimeException e) { + logger.debug("Error parsing API Scheme: {} ", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, "Error: " + e.getMessage()); + this.online = false; + } catch (final TimeoutException | ExecutionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error: " + e.getMessage()); + this.online = false; + } + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + final ApiPageEntry e = this.entries.get(channelUID.getId()); + if (command instanceof RefreshType) { + if (e == null) { + // This might be a race condition between the 'initial' poll / fetch not finished yet or the channel + // might have been deleted in between. When the initial poll is still in progress, it will send an + // update for the channel as soon as we have the data. If the channel got deleted, there is nothing we + // can do. + return; + } + // we have our ApiPageEntry which also holds our last known state - just update it. + updateState(channelUID, e.getLastState()); + return; + } + if (e == null) { + logger.debug("Got command for unknown channel {}: {}", channelUID, command); + return; + } + final Request reqUpdate; + switch (e.type) { + case SWITCH_BUTTON: + reqUpdate = prepareRequest(buildUri("INCLUDE/change.cgi?changeadrx2=" + e.address + "&changetox2=" + + (command == OnOffType.ON ? "1" : "0"))); + reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required... + break; + case SWITCH_FORM: + ChangerX2Entry cx2e = e.changerX2Entry; + if (cx2e != null) { + reqUpdate = prepareRequest(buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2e.address + + "&changetox2=" + (command == OnOffType.ON ? "1" : "0"))); + reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required... + } else { + logger.debug("Got command for uninitalized channel {}: {}", channelUID, command); + return; + } + break; + case STATE_FORM: + ChangerX2Entry cx2sf = e.changerX2Entry; + if (cx2sf != null) { + String val = cx2sf.options.get(((StringType) command).toFullString()); + if (val != null) { + reqUpdate = prepareRequest( + buildUri("INCLUDE/change.cgi?changeadrx2=" + cx2sf.address + "&changetox2=" + val)); + reqUpdate.header(HttpHeader.REFERER, this.serverBase + "schema.html"); // required... + } else { + logger.warn("Got unknown form command {} for channel {}; Valid commands are: {}", command, + channelUID, cx2sf.options.keySet()); + return; + } + } else { + logger.debug("Got command for uninitalized channel {}: {}", channelUID, command); + return; + } + break; + case READ_ONLY_NUMERIC: + case READ_ONLY_STATE: + case READ_ONLY_SWITCH: + logger.debug("Got command for ReadOnly channel {}: {}", channelUID, command); + return; + default: + logger.debug("Got command for unhandled type {} channel {}: {}", e.type, channelUID, command); + return; + } + try { + ContentResponse res = reqUpdate.send(); + if (res.getStatus() == 200) { + // update ok, we update the state + e.setLastState((State) command); + updateState(channelUID, (State) command); + } else { + logger.warn("Error sending update for {} = {}: {} {}", channelUID, command, res.getStatus(), + res.getReason()); + } + } catch (InterruptedException | TimeoutException | ExecutionException ex) { + logger.warn("Error sending update for {} = {}: {}", channelUID, command, ex.getMessage()); + } + } + + // make it accessible for ApiPageParser + @Override + protected void updateState(final ChannelUID channelUID, final State state) { + super.updateState(channelUID, state); + } + + @Override + public void dispose() { + final ScheduledFuture scheduledFuture = this.scheduledFuture; + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + this.scheduledFuture = null; + } + super.dispose(); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..ec0ee0d1029ea --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + TA C.M.I. Binding + This is the binding for TA C.M.I. + Timo Wendt, Wolfgang Klimt, Christian Niessner + + diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/bridge.xml b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/bridge.xml new file mode 100644 index 0000000000000..7cbed97cc1e2e --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/bridge.xml @@ -0,0 +1,11 @@ + + + + + + This bridge opens the CoE-UDP Port 5441 on OpenHAB for communication with "Technische Alternative C.M.I." + + diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..32ac895102982 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + CoE Communication to the "Technische Alternative C.M.I. Control and Monitoring Interface" + + + + + Host name of IP address of the CMI + network-address + + + + The CoE / CAN Node number openHAB should represent + + + + + + + + Switch + + A digital channel sent from C.M.I. to openHAB + + + + + C.M.I. Network Output + + + + + Switch + + A digital channel sent from OpenHAB to C.M.I. + + + + Network Output + + + + Initial value to set after startup (optional, defaults to uninitialized) + + + + + + Number + + A Analog Channel received from the C.M.I. + + + + + C.M.I. Network Output + + + + + Number + + A Analog Channel sent to the C.M.I. + + + + Network Output + + + + Measurement type for this channel + + + + + + + + + + + + + + + + + + + + + + + + + + + + Initial value to set after startup (optional, defaults to uninitialized) + + + + + + + Communication to a special "API" schema page on a "Technische Alternative C.M.I. Control and Monitoring + Interface" + + + + + Host name or IP address of the C.M.I. + network-address + + + + Username for authentication on the C.M.I. + + + + Password for authentication on the C.M.I. + password + + + + ID of the schema API page + + + + Poll interval (in seconds) how often to poll the API Page + 10 + true + + + + + + + Switch + + An On/Off state read from C.M.I. + + + + Switch + + A modifiable On/Off state read from C.M.I. + + + Number + + A numeric value read from C.M.I. + + + + String + + A state value read from C.M.I. + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index ec5165810e42a..a1e2f3bde37cd 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -254,6 +254,7 @@ org.openhab.binding.squeezebox org.openhab.binding.synopanalyzer org.openhab.binding.systeminfo + org.openhab.binding.tacmi org.openhab.binding.tado org.openhab.binding.tankerkoenig org.openhab.binding.telegram