]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mikrotik] Mikrotik RouterOS Binding - Initial contribution (#10014)
authorOleg Vivtash <oleg@vivtash.net>
Tue, 21 Sep 2021 17:40:57 +0000 (20:40 +0300)
committerGitHub <noreply@github.com>
Tue, 21 Sep 2021 17:40:57 +0000 (19:40 +0200)
* [mikrotik] Initial contribution
Build fix
Linter concerns fixed
Post-review updates
Apply suggestions from code review

Co-authored-by: Matthew Skinner <matt@pcmus.com>
[mikrotik] Byte channels UOM update
[mikrotik] UOM updates; minor improvements
[mikrotik] ThingTypes update

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Version bump, bugfix (thanks @radokristof)

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Raw uptime channel removed

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Traces removed

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Readme update

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] More Null checks

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] thing-types update

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Units update

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Readme signal update

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Rate channels unit fix

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Work on codestyle report and some compiler warnings

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] No more compiler warnings

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
* [mikrotik] Minus null check

Signed-off-by: Oleg Vivtash <oleg@vivtash.net>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
40 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.mikrotik/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/README.md [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/ConfigValidation.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/InterfaceThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/RouterosThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/WirelessClientThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/ChannelUpdateException.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikBaseThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikInterfaceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikRouterosBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikWirelessClientThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBaseData.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBridgeInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapsmanRegistration.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosEthernetInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosInterfaceBase.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosInterfaceType.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosL2TPCliInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosL2TPSrvInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosPPPoECliInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosRegistrationBase.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosRouterboardInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosSystemResources.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosWirelessRegistration.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosWlanInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/Converter.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/RateCalculator.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/StateUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.mikrotik/src/test/java/org/openhab/binding/mikrotik/internal/util/ConverterTest.java [new file with mode: 0644]
bundles/pom.xml

index 5471b3eb9ad72fc0604cde8650fbb21a5f7dc13b..a9bb24764bb513b2e6ae3f0e80ab86735bdac035 100644 (file)
 /bundles/org.openhab.binding.mielecloud/ @BjoernLange
 /bundles/org.openhab.binding.mihome/ @pboos
 /bundles/org.openhab.binding.miio/ @marcelrv
+/bundles/org.openhab.binding.mikrotik/ @duhast
 /bundles/org.openhab.binding.milight/ @davidgraeff
 /bundles/org.openhab.binding.millheat/ @seime
 /bundles/org.openhab.binding.minecraft/ @ibaton
index 3c984e667d6508297d62fdb89d73532407a61731..c86eafdcd7a819ffea3b0eeb3b88806725cc4efc 100644 (file)
       <artifactId>org.openhab.binding.miio</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.mikrotik</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.milight</artifactId>
diff --git a/bundles/org.openhab.binding.mikrotik/NOTICE b/bundles/org.openhab.binding.mikrotik/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+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
diff --git a/bundles/org.openhab.binding.mikrotik/README.md b/bundles/org.openhab.binding.mikrotik/README.md
new file mode 100644 (file)
index 0000000..43d74e6
--- /dev/null
@@ -0,0 +1,374 @@
+# Mikrotik RouterOS Binding
+
+This binding integrates [Mikrotik](https://mikrotik.com/) [RouterOS](https://help.mikrotik.com/docs/display/ROS/RouterOS)
+[devices](https://mikrotik.com/products) allowing monitoring of system resources, network interfaces and WiFi clients.
+
+## Supported Things
+
+* `routeros` - An instance of the RouterOS device connection
+* `interface` - A network interface inside RouterOS device
+* `wifiRegistration` - Any wireless client connected to a RouterOS wireless network (regular or CAPsMAN-managed)
+
+
+## Discovery
+
+Discovery is currently not supported, but may be implemented in future versions.
+
+
+## Bridge Configuration
+
+To use this binding you need at least one RouterOS-powered device (Bridge) accessible to the host running
+openHAB via network.
+Make sure your RouterOS has the API enabled by visiting [<kbd>IP -> Services</kbd>](https://wiki.mikrotik.com/wiki/Manual:IP/Services) 
+configuration section in 
+[WinBox](https://wiki.mikrotik.com/wiki/Manual:Winbox).
+Take note of the API port number as you'll need it below.
+[SSL API connection](https://wiki.mikrotik.com/wiki/Manual:API-SSL) is not yet supported by this binding.
+To connect to the RouterOS API, you will need to provide user credentials for the bridge thing. 
+You may use your current credentials that you use to manage your devices, but it is highly recommended to **create a read-only RouterOS user** since this binding only need to read data from the device.
+To do this, proceed to <kbd>System -> Users</kbd> configuration section and add a user to the `read` group.
+
+> Thing type: `routeros`
+
+The RouterOS Bridge configuration parameters are:
+
+| Name | Type | Required | Default | Description |
+|---|---|---|---|---|
+| host | text | Yes | 192.168.88.1 | Hostname or IP address of the RouterOS device |
+| port | integer | No | 8728 | API Port number of the RouterOS device |
+| login | text | Yes | admin | The username to access the the RouterOS device |
+| password | text | Yes |  | The user password to access the RouterOS device |
+| refresh | integer | No | 10 | The refresh interval in seconds to poll the RouterOS device |
+
+**All things provided by this binding require a working bridge to be set up.**
+
+
+### Bridge Channels
+
+| Channel | Type | Description | Comment |
+|---|---|---|---|
+| freeSpace | Number:DataAmount | Amount of free storage left on device in bytes |  |
+| totalSpace | Number:DataAmount | Amount of total storage available on device in bytes |  |
+| usedSpace | Number:Dimensionless | Percentage of used device storage space |  |
+| freeMemory | Number:DataAmount | Amount of free memory left on device in bytes |  |
+| totalMemory | Number:DataAmount | Amount of total memory available on device in bytes |  |
+| usedMemory | Number:Dimensionless | Percentage of used device memory |  |
+| cpuLoad | Number:Dimensionless | CPU load percentage |  |
+| upSince | DateTime | Time when thing got up |  |
+
+
+
+## WiFi Client Thing Configuration
+
+> Thing type: `wifiRegistration`
+
+Represents a wireless client connected to a RouterOS wireless network (direct or CAPsMAN-managed).
+
+The WiFi client thing configuration parameters are:
+
+| Name | Type | Required | Default | Description |
+|---|---|---|---|---|
+| mac | text | Yes |  | WiFi client MAC address |
+| ssid | text | No |  | Constraining SSID for the WiFi client (optional). If client will connect to another SSID, this thing will stay offline until client reconnects to specified SSID. |
+| considerContinuous | integer | No | 180 | The interval in seconds to treat the client as connected permanently |
+
+### WiFi client Thing Channels
+
+| Channel | Type | Description | Comment |
+|---|---|---|---|
+| macAddress | String | MAC address of the client or interface |  |
+| comment | String | User-defined comment |  |
+| connected | Switch | Reflects connected or disconnected state |  |
+| continuous | Switch | Connection is considered long-running |  |
+| ssid | String | Wireless Network (SSID) the wireless client is connected to |  |
+| interface | String | Network interface name |  |
+| signal | system.signal-strength | Signal strength (RSSI) |  |
+| upSince | DateTime | Time when thing got up |  |
+| lastSeen | DateTime | Time of when the client was last seen connected |  |
+| txRate | Number:DataTransferRate | Rate of data transmission in megabits per second |  |
+| rxRate | Number:DataTransferRate | Rate of data receiving in megabits per second |  |
+| txPacketRate | Number | Rate of data transmission in packets per second |  |
+| rxPacketRate | Number | Rate of data receiving in packets per second |  |
+| txBytes | Number:DataAmount | Amount of bytes transmitted |  |
+| rxBytes | Number:DataAmount | Amount of bytes received |  |
+| txPackets | Number | Amount of packets transmitted |  |
+| rxPackets | Number | Amount of packets received |  |
+
+## Network Interface Thing Configuration
+
+> Thing type: `interface`
+
+Represents a network interface from RouterOS system (ethernet, wifi, vpn, etc.)
+At the moment the binding supports the following RouterOS interface types:
+
+* `ether`
+* `bridge`
+* `wlan`
+* `cap`
+* `pppoe-out`
+* `l2tp-in`
+* `l2tp-out`
+
+The interface thing configuration parameters are:
+
+### Interface Thing Configuration
+
+| Name | Type | Required | Default | Description |
+|---|---|---|---|---|
+| name | text | Yes |  | RouterOS Interface name (i.e. ether1) |
+
+### Interface Thing Channels
+
+Please note that different on RouterOS interfaces has different data available depending on the kind of interface.
+While the common dataset is same, some specific information for specific interface type may be missing. This may
+be improved in future binding versions.
+
+Common for all kinds of interfaces:
+
+| Channel | Type | Description | Comment |
+|---|---|---|---|
+| type | String | Network interface type |  |
+| name | String | Network interface name |  |
+| comment | String | User-defined comment |  |
+| macAddress | String | MAC address of the client or interface |  |
+| enabled | Switch | Reflects enabled or disabled state |  |
+| connected | Switch | Reflects connected or disconnected state |  |
+| lastLinkDownTime | DateTime | Last time when link went down |  |
+| lastLinkUpTime | DateTime | Last time when link went up |  |
+| linkDowns | Number | Amount of link downs |  |
+| txRate | Number:DataTransferRate | Rate of data transmission in megabits per second |  |
+| rxRate | Number:DataTransferRate | Rate of data receiving in megabits per second |  |
+| txPacketRate | Number | Rate of data transmission in packets per second |  |
+| rxPacketRate | Number | Rate of data receiving in packets per second |  |
+| txBytes | Number:DataAmount | Amount of bytes transmitted |  |
+| rxBytes | Number:DataAmount | Amount of bytes received |  |
+| txPackets | Number | Amount of packets transmitted |  |
+| rxPackets | Number | Amount of packets received |  |
+| txDrops | Number | Amount of packets dropped during transmission |  |
+| rxDrops | Number | Amount of packets dropped during receiving |  |
+| txErrors | Number | Amount of errors during transmission |  |
+| rxErrors | Number | Amount of errors during receiving |  |
+| defaultName | String | Interface factory name | Populated only for `ether` interfaces |
+| rate | String | Ethernet link rate | Populated only for `ether` interfaces |
+| state | String | WiFi interface state |  |
+| registeredClients | Number | Amount of clients registered to WiFi interface | Populated only for `cap` interfaces |
+| authorizedClients | Number | Amount of clients authorized by WiFi interface | Populated only for `cap` interfaces |
+| upSince | DateTime | Time when thing got up | Populated only for `cap` interfaces |
+
+## Text Configuration Example
+
+**Change config options accordingly.**
+
+_things/mikrotik.things_
+
+```
+Bridge mikrotik:routeros:rb1 "My RouterBoard" [ host="192.168.0.1", port=8728, login="openhab", password="thatsasecret", refresh=10 ] {
+       Thing interface eth1 "Eth1" [ name="ether1" ]
+       Thing interface eth2 "Eth2" [ name="ether2-wan1" ]
+       Thing interface cap1 "Cap1" [ name="cap5" ]
+       Thing interface ppp1 "PPPoE1" [ name="isp-pppoe" ]
+       Thing interface tun1 "L2TPSrv1" [ name="l2tp-parents" ]
+       Thing wifiRegistration wifi1 "Phone1" [ mac="F4:60:E2:C5:47:94", considerContinuous=60 ]
+       Thing wifiRegistration wifi2 "Tablet2" [ mac="18:1D:EA:A5:A2:9E" ]
+}
+```
+
+
+_items/mikrotik.items_
+
+```
+Group gRB1 "RB3011 System"
+Number:DataAmount   My_RB_3011_Free_Space     "Free space"     (gRB1) {channel="mikrotik:routeros:rb1:freeSpace"}
+Number:DataAmount   My_RB_3011_Total_Space    "Total space"    (gRB1) {channel="mikrotik:routeros:rb1:totalSpace"}
+Number:Dimensionless   My_RB_3011_Used_Space     "Used space"   (gRB1) {channel="mikrotik:routeros:rb1:usedSpace"}
+Number:DataAmount   My_RB_3011_Free_Memory    "Free ram"       (gRB1) {channel="mikrotik:routeros:rb1:freeMemory"}
+Number:DataAmount   My_RB_3011_Total_Memory   "Total ram"      (gRB1) {channel="mikrotik:routeros:rb1:totalMemory"}
+Number:Dimensionless   My_RB_3011_Used_Memory    "Used ram"     (gRB1) {channel="mikrotik:routeros:rb1:usedMemory"}
+Number:Dimensionless   My_RB_3011_Cpu_Load       "Cpu load"     (gRB1) {channel="mikrotik:routeros:rb1:cpuLoad"}
+DateTime   My_RB_3011_Upsince      "Up since [%1$td.%1$tm.%1$ty %1$tH:%1$tM]"         (gRB1) {channel="mikrotik:routeros:rb1:upSince"}
+
+Group gRB1Eth1 "Ethernet Interface 1"
+String     Eth_1_Type                  "Type"                       (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:type"}
+String     Eth_1_Name                  "Name"                       (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:name"}
+String     Eth_1_Comment               "Comment"                    (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:comment"}
+String     Eth_1_Mac_Address           "Mac address"                (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:macAddress"}
+Switch     Eth_1_Enabled               "Enabled"                    (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:enabled"}
+Switch     Eth_1_Connected             "Connected"                  (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:connected"}
+DateTime   Eth_1_Last_Link_Down_Time   "Last link down"             (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:lastLinkDownTime"}
+DateTime   Eth_1_Last_Link_Up_Time     "Last link up"               (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:lastLinkUpTime"}
+Number     Eth_1_Link_Downs            "Link downs"                 (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:linkDowns"}
+Number:DataTransferRate     Eth_1_Tx_Rate               "Transmission rate"          (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:txRate"}
+Number:DataTransferRate     Eth_1_Rx_Rate               "Receiving rate"             (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rxRate"}
+Number     Eth_1_Tx_Packet_Rate        "Transmission packet rate"   (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:txPacketRate"}
+Number     Eth_1_Rx_Packet_Rate        "Receiving packet rate"      (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rxPacketRate"}
+Number:DataAmount     Eth_1_Tx_Bytes              "Transmitted bytes"          (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:txBytes"}
+Number:DataAmount     Eth_1_Rx_Bytes              "Received bytes"             (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rxBytes"}
+Number     Eth_1_Tx_Packets            "Transmitted packets"        (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:txPackets"}
+Number     Eth_1_Rx_Packets            "Received packets"           (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rxPackets"}
+Number     Eth_1_Tx_Drops              "Transmission drops"         (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:txDrops"}
+Number     Eth_1_Rx_Drops              "Receiving drops"            (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rxDrops"}
+Number     Eth_1_Tx_Errors             "Transmission errors"        (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:txErrors"}
+Number     Eth_1_Rx_Errors             "Receiving errors"           (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rxErrors"}
+String     Eth_1_Default_Name          "Default name"               (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:defaultName"}
+String     Eth_1_Rate                  "Link rate"                  (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:rate"}
+String     Eth_1_Auto_Negotiation      "Auto negotiation"           (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:autoNegotiation"}
+String     Eth_1_State                 "State"                      (gRB1Eth1) {channel="mikrotik:interface:rb1:eth1:state"}
+
+Group gRB1Eth2 "Ethernet Interface 2"
+String     Eth_2_Type                  "Type"                       (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:type"}
+String     Eth_2_Name                  "Name"                       (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:name"}
+String     Eth_2_Comment               "Comment"                    (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:comment"}
+String     Eth_2_Mac_Address           "Mac address"                (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:macAddress"}
+Switch     Eth_2_Enabled               "Enabled"                    (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:enabled"}
+Switch     Eth_2_Connected             "Connected"                  (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:connected"}
+DateTime   Eth_2_Last_Link_Down_Time   "Last link down"             (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:lastLinkDownTime"}
+DateTime   Eth_2_Last_Link_Up_Time     "Last link up"               (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:lastLinkUpTime"}
+Number     Eth_2_Link_Downs            "Link downs"                 (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:linkDowns"}
+Number:DataTransferRate     Eth_2_Tx_Rate               "Transmission rate"          (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:txRate"}
+Number:DataTransferRate     Eth_2_Rx_Rate               "Receiving rate"             (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rxRate"}
+Number     Eth_2_Tx_Packet_Rate        "Transmission packet rate"   (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:txPacketRate"}
+Number     Eth_2_Rx_Packet_Rate        "Receiving packet rate"      (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rxPacketRate"}
+Number:DataAmount     Eth_2_Tx_Bytes              "Transmitted bytes"          (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:txBytes"}
+Number:DataAmount     Eth_2_Rx_Bytes              "Received bytes"             (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rxBytes"}
+Number     Eth_2_Tx_Packets            "Transmitted packets"        (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:txPackets"}
+Number     Eth_2_Rx_Packets            "Received packets"           (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rxPackets"}
+Number     Eth_2_Tx_Drops              "Transmission drops"         (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:txDrops"}
+Number     Eth_2_Rx_Drops              "Receiving drops"            (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rxDrops"}
+Number     Eth_2_Tx_Errors             "Transmission errors"        (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:txErrors"}
+Number     Eth_2_Rx_Errors             "Receiving errors"           (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rxErrors"}
+String     Eth_2_Default_Name          "Default name"               (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:defaultName"}
+String     Eth_2_Rate                  "Link rate"                  (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:rate"}
+String     Eth_2_Auto_Negotiation      "Auto negotiation"           (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:autoNegotiation"}
+String     Eth_2_State                 "State"                      (gRB1Eth2) {channel="mikrotik:interface:rb1:eth2:state"}
+
+Group gRB1Cap1 "CAPsMAN Inerface 1"
+String     Cap_1_Type                  "Type"                       (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:type"}
+String     Cap_1_Name                  "Name"                       (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:name"}
+String     Cap_1_Comment               "Comment"                    (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:comment"}
+String     Cap_1_Mac_Address           "Mac address"                (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:macAddress"}
+Switch     Cap_1_Enabled               "Enabled"                    (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:enabled"}
+Switch     Cap_1_Connected             "Connected"                  (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:connected"}
+DateTime   Cap_1_Last_Link_Down_Time   "Last link down"             (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:lastLinkDownTime"}
+DateTime   Cap_1_Last_Link_Up_Time     "Last link up"               (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:lastLinkUpTime"}
+Number     Cap_1_Link_Downs            "Link downs"                 (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:linkDowns"}
+Number:DataTransferRate     Cap_1_Tx_Rate               "Transmission rate"          (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:txRate"}
+Number:DataTransferRate     Cap_1_Rx_Rate               "Receiving rate"             (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:rxRate"}
+Number     Cap_1_Tx_Packet_Rate        "Transmission packet rate"   (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:txPacketRate"}
+Number     Cap_1_Rx_Packet_Rate        "Receiving packet rate"      (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:rxPacketRate"}
+Number:DataAmount     Cap_1_Tx_Bytes              "Transmitted bytes"          (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:txBytes"}
+Number:DataAmount     Cap_1_Rx_Bytes              "Received bytes"             (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:rxBytes"}
+Number     Cap_1_Tx_Packets            "Transmitted packets"        (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:txPackets"}
+Number     Cap_1_Rx_Packets            "Received packets"           (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:rxPackets"}
+Number     Cap_1_Tx_Drops              "Transmission drops"         (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:txDrops"}
+Number     Cap_1_Rx_Drops              "Receiving drops"            (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:rxDrops"}
+Number     Cap_1_Tx_Errors             "Transmission errors"        (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:txErrors"}
+Number     Cap_1_Rx_Errors             "Receiving errors"           (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:rxErrors"}
+String     Cap_1_State                 "State"                      (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:state"}
+Number     Cap_1_Registered_Clients    "Registered clients"         (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:registeredClients"}
+Number     Cap_1_Authorized_Clients    "Authorized clients"         (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:authorizedClients"}
+DateTime   Cap_1_Up_Since              "Up since"                   (gRB1Cap1) {channel="mikrotik:interface:rb1:cap1:upSince"}
+
+Group gRB1Ppp1 "PPPoE Client 1"
+String     PP_Po_E_1_Type                  "Type"                       (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:type"}
+String     PP_Po_E_1_Name                  "Name"                       (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:name"}
+String     PP_Po_E_1_Comment               "Comment"                    (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:comment"}
+String     PP_Po_E_1_Mac_Address           "Mac address"                (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:macAddress"}
+Switch     PP_Po_E_1_Enabled               "Enabled"                    (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:enabled"}
+Switch     PP_Po_E_1_Connected             "Connected"                  (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:connected"}
+DateTime   PP_Po_E_1_Last_Link_Down_Time   "Last link down"             (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:lastLinkDownTime"}
+DateTime   PP_Po_E_1_Last_Link_Up_Time     "Last link up"               (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:lastLinkUpTime"}
+Number     PP_Po_E_1_Link_Downs            "Link downs"                 (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:linkDowns"}
+Number:DataTransferRate     PP_Po_E_1_Tx_Rate               "Transmission rate"          (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:txRate"}
+Number:DataTransferRate     PP_Po_E_1_Rx_Rate               "Receiving rate"             (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:rxRate"}
+Number     PP_Po_E_1_Tx_Packet_Rate        "Transmission packet rate"   (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:txPacketRate"}
+Number     PP_Po_E_1_Rx_Packet_Rate        "Receiving packet rate"      (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:rxPacketRate"}
+Number:DataAmount     PP_Po_E_1_Tx_Bytes              "Transmitted bytes"          (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:txBytes"}
+Number:DataAmount     PP_Po_E_1_Rx_Bytes              "Received bytes"             (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:rxBytes"}
+Number     PP_Po_E_1_Tx_Packets            "Transmitted packets"        (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:txPackets"}
+Number     PP_Po_E_1_Rx_Packets            "Received packets"           (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:rxPackets"}
+Number     PP_Po_E_1_Tx_Drops              "Transmission drops"         (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:txDrops"}
+Number     PP_Po_E_1_Rx_Drops              "Receiving drops"            (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:rxDrops"}
+Number     PP_Po_E_1_Tx_Errors             "Transmission errors"        (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:txErrors"}
+Number     PP_Po_E_1_Rx_Errors             "Receiving errors"           (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:rxErrors"}
+String     PP_Po_E_1_State                 "State"                      (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:state"}
+DateTime   PP_Po_E_1_Up_Since              "Up since"                   (gRB1Ppp1) {channel="mikrotik:interface:rb1:ppp1:upSince"}
+
+Group gRB1Tun1 "L2TP Server 1"
+String     L_2_TP_Srv_1_Type                  "Type"                       (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:type"}
+String     L_2_TP_Srv_1_Name                  "Name"                       (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:name"}
+String     L_2_TP_Srv_1_Comment               "Comment"                    (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:comment"}
+String     L_2_TP_Srv_1_Mac_Address           "Mac address"                (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:macAddress"}
+Switch     L_2_TP_Srv_1_Enabled               "Enabled"                    (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:enabled"}
+Switch     L_2_TP_Srv_1_Connected             "Connected"                  (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:connected"}
+DateTime   L_2_TP_Srv_1_Last_Link_Down_Time   "Last link down"             (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:lastLinkDownTime"}
+DateTime   L_2_TP_Srv_1_Last_Link_Up_Time     "Last link up"               (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:lastLinkUpTime"}
+Number     L_2_TP_Srv_1_Link_Downs            "Link downs"                 (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:linkDowns"}
+Number:DataTransferRate     L_2_TP_Srv_1_Tx_Rate               "Transmission rate"          (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:txRate"}
+Number:DataTransferRate     L_2_TP_Srv_1_Rx_Rate               "Receiving rate"             (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:rxRate"}
+Number     L_2_TP_Srv_1_Tx_Packet_Rate        "Transmission packet rate"   (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:txPacketRate"}
+Number     L_2_TP_Srv_1_Rx_Packet_Rate        "Receiving packet rate"      (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:rxPacketRate"}
+Number:DataAmount     L_2_TP_Srv_1_Tx_Bytes              "Transmitted bytes"          (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:txBytes"}
+Number:DataAmount     L_2_TP_Srv_1_Rx_Bytes              "Received bytes"             (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:rxBytes"}
+Number     L_2_TP_Srv_1_Tx_Packets            "Transmitted packets"        (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:txPackets"}
+Number     L_2_TP_Srv_1_Rx_Packets            "Received packets"           (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:rxPackets"}
+Number     L_2_TP_Srv_1_Tx_Drops              "Transmission drops"         (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:txDrops"}
+Number     L_2_TP_Srv_1_Rx_Drops              "Receiving drops"            (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:rxDrops"}
+Number     L_2_TP_Srv_1_Tx_Errors             "Transmission errors"        (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:txErrors"}
+Number     L_2_TP_Srv_1_Rx_Errors             "Receiving errors"           (gRB1Tun1) {channel="mikrotik:interface:rb1:tun1:rxErrors"}
+
+Group gRB1Wifi1 "WiFi Client 1"
+String     Phone_1_Mac_Address      "Mac address"                          (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:macAddress"}
+String     Phone_1_Comment          "Comment"                              (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:comment"}
+Switch     Phone_1_Connected        "Connected"                            (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:connected"}
+Switch     Phone_1_Continuous       "Continuous"                           (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:continuous"}
+String     Phone_1_Ssid             "Wi fi network"                        (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:ssid"}
+String     Phone_1_Interface        "Name"                                 (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:interface"}
+Number     Phone_1_Signal           "Received signal strength indicator"   (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:signal"}
+DateTime   Phone_1_Up_Since         "Up since"                             (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:upSince"}
+DateTime   Phone_1_Last_Seen        "Last seen"                            (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:lastSeen"}
+Number:DataTransferRate     Phone_1_Tx_Rate          "Transmission rate"                    (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:txRate"}
+Number:DataTransferRate     Phone_1_Rx_Rate          "Receiving rate"                       (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:rxRate"}
+Number     Phone_1_Tx_Packet_Rate   "Transmission packet rate"             (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:txPacketRate"}
+Number     Phone_1_Rx_Packet_Rate   "Receiving packet rate"                (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:rxPacketRate"}
+Number:DataAmount     Phone_1_Tx_Bytes         "Transmitted bytes"                    (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:txBytes"}
+Number:DataAmount     Phone_1_Rx_Bytes         "Received bytes"                       (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:rxBytes"}
+Number     Phone_1_Tx_Packets       "Transmitted packets"                  (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:txPackets"}
+Number     Phone_1_Rx_Packets       "Received packets"                     (gRB1Wifi1) {channel="mikrotik:wifiRegistration:rb1:wifi1:rxPackets"}
+
+Group gRB1Wifi2 "WiFi Client 2"
+String     Tablet_2_Mac_Address      "Mac address"                          (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:macAddress"}
+String     Tablet_2_Comment          "Comment"                              (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:comment"}
+Switch     Tablet_2_Connected        "Connected"                            (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:connected"}
+Switch     Tablet_2_Continuous       "Continuous"                           (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:continuous"}
+String     Tablet_2_Ssid             "Wi fi network"                        (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:ssid"}
+String     Tablet_2_Interface        "Name"                                 (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:interface"}
+Number     Tablet_2_Signal           "Received signal strength indicator"   (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:signal"}
+DateTime   Tablet_2_Up_Since         "Up since"                             (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:upSince"}
+DateTime   Tablet_2_Last_Seen        "Last seen"                            (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:lastSeen"}
+Number:DataTransferRate    Tablet_2_Tx_Rate          "Transmission rate"                    (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:txRate"}
+Number:DataTransferRate     Tablet_2_Rx_Rate          "Receiving rate"                       (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:rxRate"}
+Number     Tablet_2_Tx_Packet_Rate   "Transmission packet rate"             (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:txPacketRate"}
+Number     Tablet_2_Rx_Packet_Rate   "Receiving packet rate"                (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:rxPacketRate"}
+Number:DataAmount     Tablet_2_Tx_Bytes         "Transmitted bytes"                    (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:txBytes"}
+Number:DataAmount     Tablet_2_Rx_Bytes         "Received bytes"                       (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:rxBytes"}
+Number     Tablet_2_Tx_Packets       "Transmitted packets"                  (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:txPackets"}
+Number     Tablet_2_Rx_Packets       "Received packets"                     (gRB1Wifi2) {channel="mikrotik:wifiRegistration:rb1:wifi2:rxPackets"}
+```
+
+_sitemaps/mikrotik.sitemap_
+
+```
+sitemap mikrotik label="Mikrotik Binding Demo"
+{
+       Frame label="RouterBOARD 1" {
+               Group item=gRB1
+               Group item=gRB1Eth1
+               Group item=gRB1Eth2
+               Group item=gRB1Ppp1
+               Group item=gRB1Tun1
+               Group item=gRB1Cap1
+               Group item=gRB1Wifi1
+               Group item=gRB1Wifi2
+       }
+}
+```
diff --git a/bundles/org.openhab.binding.mikrotik/pom.xml b/bundles/org.openhab.binding.mikrotik/pom.xml
new file mode 100644 (file)
index 0000000..08a7da4
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.mikrotik</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Mikrotik Binding</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>me.legrange</groupId>
+      <artifactId>mikrotik</artifactId>
+      <version>3.0.7</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/feature/feature.xml b/bundles/org.openhab.binding.mikrotik/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..50de433
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.mikrotik-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-mikrotik" description="Mikrotik Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mikrotik/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikBindingConstants.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikBindingConstants.java
new file mode 100644 (file)
index 0000000..4463c40
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MikrotikBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class MikrotikBindingConstants {
+
+    private static final String BINDING_ID = "mikrotik";
+
+    public static final String PROPERTY_MODEL = "modelId";
+    public static final String PROPERTY_FIRMWARE = "firmware";
+    public static final String PROPERTY_SERIAL_NUMBER = "serial";
+
+    // List of all Thing Types
+    public static final ThingTypeUID THING_TYPE_ROUTEROS = new ThingTypeUID(BINDING_ID, "routeros");
+    public static final ThingTypeUID THING_TYPE_INTERFACE = new ThingTypeUID(BINDING_ID, "interface");
+    public static final ThingTypeUID THING_TYPE_WIRELESS_CLIENT = new ThingTypeUID(BINDING_ID, "wifiRegistration");
+
+    // RouterOS system stats
+    public static final String CHANNEL_FREE_SPACE = "freeSpace";
+    public static final String CHANNEL_TOTAL_SPACE = "totalSpace";
+    public static final String CHANNEL_USED_SPACE = "usedSpace";
+    public static final String CHANNEL_FREE_MEM = "freeMemory";
+    public static final String CHANNEL_TOTAL_MEM = "totalMemory";
+    public static final String CHANNEL_USED_MEM = "usedMemory";
+    public static final String CHANNEL_CPU_LOAD = "cpuLoad";
+
+    public static final String CHANNEL_COMMENT = "comment";
+
+    // List of common interface channels
+    public static final String CHANNEL_NAME = "name";
+    public static final String CHANNEL_TYPE = "type";
+    public static final String CHANNEL_MAC = "macAddress";
+    public static final String CHANNEL_ENABLED = "enabled";
+    public static final String CHANNEL_CONNECTED = "connected"; // used for wifi client as well
+    public static final String CHANNEL_LAST_LINK_DOWN_TIME = "lastLinkDownTime";
+    public static final String CHANNEL_LAST_LINK_UP_TIME = "lastLinkUpTime";
+    public static final String CHANNEL_LINK_DOWNS = "linkDowns";
+    public static final String CHANNEL_TX_DATA_RATE = "txRate";
+    public static final String CHANNEL_RX_DATA_RATE = "rxRate";
+    public static final String CHANNEL_TX_PACKET_RATE = "txPacketRate";
+    public static final String CHANNEL_RX_PACKET_RATE = "rxPacketRate";
+    public static final String CHANNEL_TX_BYTES = "txBytes";
+    public static final String CHANNEL_RX_BYTES = "rxBytes";
+    public static final String CHANNEL_TX_PACKETS = "txPackets";
+    public static final String CHANNEL_RX_PACKETS = "rxPackets";
+    public static final String CHANNEL_TX_DROPS = "txDrops";
+    public static final String CHANNEL_RX_DROPS = "rxDrops";
+    public static final String CHANNEL_TX_ERRORS = "txErrors";
+    public static final String CHANNEL_RX_ERRORS = "rxErrors";
+
+    // Ethernet interface channel list
+    public static final String CHANNEL_DEFAULT_NAME = "defaultName";
+    public static final String CHANNEL_RATE = "rate";
+
+    // CAPsMAN interface channel list
+    public static final String CHANNEL_INTERFACE = "interface";
+    public static final String CHANNEL_STATE = "state";
+    public static final String CHANNEL_REGISTERED_CLIENTS = "registeredClients";
+    public static final String CHANNEL_AUTHORIZED_CLIENTS = "authorizedClients";
+    public static final String CHANNEL_CONTINUOUS = "continuous";
+
+    // PPP interface shared channel list
+    public static final String CHANNEL_UP_SINCE = "upSince";
+
+    // Wireless client channels
+    public static final String CHANNEL_LAST_SEEN = "lastSeen";
+    public static final String CHANNEL_SSID = "ssid";
+    public static final String CHANNEL_SIGNAL = "signal";
+
+    // List of common wired + wireless client channels
+    public static final String CHANNEL_SITE = "site";
+    public static final String CHANNEL_IP_ADDRESS = "ipAddress";
+    public static final String CHANNEL_BLOCKED = "blocked";
+    public static final String CHANNEL_RECONNECT = "reconnect";
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikHandlerFactory.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikHandlerFactory.java
new file mode 100644 (file)
index 0000000..2a3f9cc
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.handler.MikrotikInterfaceThingHandler;
+import org.openhab.binding.mikrotik.internal.handler.MikrotikRouterosBridgeHandler;
+import org.openhab.binding.mikrotik.internal.handler.MikrotikWirelessClientThingHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link MikrotikHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.mikrotik", service = ThingHandlerFactory.class)
+public class MikrotikHandlerFactory extends BaseThingHandlerFactory {
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return MikrotikRouterosBridgeHandler.supportsThingType(thingTypeUID)
+                || MikrotikWirelessClientThingHandler.supportsThingType(thingTypeUID)
+                || MikrotikInterfaceThingHandler.supportsThingType(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+        if (MikrotikRouterosBridgeHandler.supportsThingType(thingTypeUID)) {
+            return new MikrotikRouterosBridgeHandler((Bridge) thing);
+        } else if (MikrotikWirelessClientThingHandler.supportsThingType(thingTypeUID)) {
+            return new MikrotikWirelessClientThingHandler(thing);
+        } else if (MikrotikInterfaceThingHandler.supportsThingType(thingTypeUID)) {
+            return new MikrotikInterfaceThingHandler(thing);
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/ConfigValidation.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/ConfigValidation.java
new file mode 100644 (file)
index 0000000..e4133bd
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ConfigValidation} interface should be implemented by all config objects, so the thing handlers could
+ * change their state properly, based on the config validation result.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public interface ConfigValidation {
+    boolean isValid();
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/InterfaceThingConfig.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/InterfaceThingConfig.java
new file mode 100644 (file)
index 0000000..5a20255
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link InterfaceThingConfig} class contains fields mapping thing configuration parameters for
+ * network interface things.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class InterfaceThingConfig implements ConfigValidation {
+    public String name = "";
+
+    public boolean isValid() {
+        return !name.isBlank();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("InterfaceThingConfig{name=%s}", name);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/RouterosThingConfig.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/RouterosThingConfig.java
new file mode 100644 (file)
index 0000000..54cf094
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RouterosThingConfig} class contains fields mapping thing configuration parameters for
+ * RouterOS bridge thing.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosThingConfig implements ConfigValidation {
+    public String host = "rb3011";
+    public int port = 8728;
+    public String login = "admin";
+    public String password = "";
+    public int refresh = 10;
+
+    public boolean isValid() {
+        return !host.isBlank() && !login.isBlank() && !password.isBlank() && refresh > 0 && port > 0;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RouterosThingConfig{host=%s, port=%d, login=%s, password=*****, refresh=%ds}", host, port,
+                login, refresh);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/WirelessClientThingConfig.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/WirelessClientThingConfig.java
new file mode 100644 (file)
index 0000000..14cac0e
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link WirelessClientThingConfig} class contains fields mapping thing configuration parameters for
+ * WiFi client thing.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class WirelessClientThingConfig implements ConfigValidation {
+    public String mac = "";
+    public String ssid = "";
+    public int considerContinuous = 180;
+
+    public boolean isValid() {
+        return !mac.isBlank() && considerContinuous > 0;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("WirelessClientThingConfig{mac=%s, ssid=%s, considerContinuous=%ds}", mac, ssid,
+                considerContinuous);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/ChannelUpdateException.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/ChannelUpdateException.java
new file mode 100644 (file)
index 0000000..8e53619
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * The {@link ChannelUpdateException} is used to bubble up channel update errors which are mainly
+ * happens during data conversion. But those errors should not bring bridge offline and break normal
+ * operation.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelUpdateException extends RuntimeException {
+    static final long serialVersionUID = 1L;
+
+    private final ThingUID thingUID;
+    private final ChannelUID channelID;
+
+    public ChannelUpdateException(ThingUID thingUID, ChannelUID channelUID, Throwable cause) {
+        super(cause);
+        this.thingUID = thingUID;
+        this.channelID = channelUID;
+    }
+
+    @Override
+    public @Nullable String getMessage() {
+        return String.format("%s @ %s/%s", super.getMessage(), thingUID, channelID);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikBaseThingHandler.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikBaseThingHandler.java
new file mode 100644 (file)
index 0000000..87bdbae
--- /dev/null
@@ -0,0 +1,246 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.handler;
+
+import static org.openhab.core.thing.ThingStatus.OFFLINE;
+import static org.openhab.core.thing.ThingStatus.ONLINE;
+import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.lang.reflect.ParameterizedType;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.config.ConfigValidation;
+import org.openhab.binding.mikrotik.internal.config.RouterosThingConfig;
+import org.openhab.binding.mikrotik.internal.model.RouterosDevice;
+import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MikrotikBaseThingHandler} is a base class for all other RouterOS things of map-value nature.
+ * It is responsible for handling commands, which are sent to one of the channels and emit channel updates
+ * whenever required.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ *
+ *
+ * @param <C> config - the config class used by this base thing handler
+ *
+ */
+@NonNullByDefault
+public abstract class MikrotikBaseThingHandler<C extends ConfigValidation> extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(MikrotikBaseThingHandler.class);
+    protected @Nullable C config;
+    private @Nullable ScheduledFuture<?> refreshJob;
+    protected ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false);
+    protected Map<String, State> currentState = new HashMap<>();
+
+    // public static boolean supportsThingType(ThingTypeUID thingTypeUID) <- in subclasses
+
+    public MikrotikBaseThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    protected @Nullable MikrotikRouterosBridgeHandler getVerifiedBridgeHandler() {
+        Bridge bridgeRef = getBridge();
+        if (bridgeRef != null && bridgeRef.getHandler() != null
+                && (bridgeRef.getHandler() instanceof MikrotikRouterosBridgeHandler)) {
+            return (MikrotikRouterosBridgeHandler) bridgeRef.getHandler();
+        }
+        return null;
+    }
+
+    protected final @Nullable RouterosDevice getRouterOs() {
+        MikrotikRouterosBridgeHandler bridgeHandler = getVerifiedBridgeHandler();
+        return bridgeHandler == null ? null : bridgeHandler.getRouteros();
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("Handling command = {} for channel = {}", command, channelUID);
+        if (getThing().getStatus() == ONLINE) {
+            RouterosDevice routeros = getRouterOs();
+            if (routeros != null) {
+                if (command == REFRESH) {
+                    refreshCache.getValue();
+                    refreshChannel(channelUID);
+                } else {
+                    try {
+                        executeCommand(channelUID, command);
+                    } catch (RuntimeException e) {
+                        logger.warn("Unexpected error handling command = {} for channel = {} : {}", command, channelUID,
+                                e.getMessage());
+                    }
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void initialize() {
+        cancelRefreshJob();
+        if (getVerifiedBridgeHandler() == null) {
+            updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "This thing requires a RouterOS bridge");
+            return;
+        }
+
+        var superKlass = (ParameterizedType) getClass().getGenericSuperclass();
+        if (superKlass == null) {
+            updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "getGenericSuperclass failed for thing handler");
+            return;
+        }
+        Class<?> klass = (Class<?>) (superKlass.getActualTypeArguments()[0]);
+
+        C localConfig = (C) getConfigAs(klass);
+        this.config = localConfig;
+
+        if (!localConfig.isValid()) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, String.format("%s is invalid", klass.getSimpleName()));
+            return;
+        }
+
+        updateStatus(ONLINE);
+    }
+
+    @Override
+    protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+        if (status == ONLINE || (status == OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR)) {
+            scheduleRefreshJob();
+        } else if (status == OFFLINE
+                && (statusDetail == ThingStatusDetail.CONFIGURATION_ERROR || statusDetail == ThingStatusDetail.GONE)) {
+            cancelRefreshJob();
+        }
+
+        // update the status only if it's changed
+        ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
+                .build();
+        if (!statusInfo.equals(getThing().getStatusInfo())) {
+            super.updateStatus(status, statusDetail, description);
+        }
+    }
+
+    @SuppressWarnings("null")
+    private void scheduleRefreshJob() {
+        synchronized (this) {
+            if (refreshJob == null) {
+                var bridgeHandler = getVerifiedBridgeHandler();
+                if (bridgeHandler == null) {
+                    updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot obtain bridge handler");
+                    return;
+                }
+                RouterosThingConfig bridgeConfig = bridgeHandler.getBridgeConfig();
+                int refreshPeriod = bridgeConfig.refresh;
+                logger.debug("Scheduling refresh job every {}s", refreshPeriod);
+
+                this.refreshCache = new ExpiringCache<>(Duration.ofSeconds(refreshPeriod), this::verifiedRefreshModels);
+                refreshJob = scheduler.scheduleWithFixedDelay(this::scheduledRun, refreshPeriod, refreshPeriod,
+                        TimeUnit.SECONDS);
+            }
+        }
+    }
+
+    private void cancelRefreshJob() {
+        synchronized (this) {
+            var job = this.refreshJob;
+            if (job != null) {
+                logger.debug("Cancelling refresh job");
+                job.cancel(true);
+                this.refreshJob = null;
+                // Not setting to null as getValue() can potentially be called after
+                this.refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false);
+            }
+        }
+    }
+
+    private void scheduledRun() {
+        MikrotikRouterosBridgeHandler bridgeHandler = getVerifiedBridgeHandler();
+        if (bridgeHandler == null) {
+            updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Failed reaching out to RouterOS bridge");
+            return;
+        }
+        Bridge bridge = getBridge();
+        if (bridge != null && bridge.getStatus() == OFFLINE) {
+            updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "The RouterOS bridge is currently offline");
+            return;
+        }
+
+        if (getThing().getStatus() != ONLINE) {
+            updateStatus(ONLINE);
+        }
+        logger.debug("Refreshing all {} channels", getThing().getUID());
+        for (Channel channel : getThing().getChannels()) {
+            try {
+                refreshChannel(channel.getUID());
+            } catch (RuntimeException e) {
+                logger.warn("Unhandled exception while refreshing the {} Mikrotik thing", getThing().getUID(), e);
+                updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    protected final void refresh() throws ChannelUpdateException {
+        if (getThing().getStatus() == ONLINE) {
+            if (getRouterOs() != null) {
+                refreshCache.getValue();
+                for (Channel channel : getThing().getChannels()) {
+                    ChannelUID channelUID = channel.getUID();
+                    try {
+                        refreshChannel(channelUID);
+                    } catch (RuntimeException e) {
+                        throw new ChannelUpdateException(getThing().getUID(), channelUID, e);
+                    }
+                }
+            }
+        }
+    }
+
+    protected boolean verifiedRefreshModels() {
+        if (getRouterOs() != null && config != null) {
+            refreshModels();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void dispose() {
+        cancelRefreshJob();
+    }
+
+    protected abstract void refreshModels();
+
+    protected abstract void refreshChannel(ChannelUID channelUID);
+
+    protected abstract void executeCommand(ChannelUID channelUID, Command command);
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikInterfaceThingHandler.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikInterfaceThingHandler.java
new file mode 100644 (file)
index 0000000..a7f251f
--- /dev/null
@@ -0,0 +1,316 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.handler;
+
+import static org.openhab.core.thing.ThingStatus.OFFLINE;
+import static org.openhab.core.thing.ThingStatus.ONLINE;
+import static org.openhab.core.thing.ThingStatusDetail.GONE;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.MikrotikBindingConstants;
+import org.openhab.binding.mikrotik.internal.config.InterfaceThingConfig;
+import org.openhab.binding.mikrotik.internal.model.RouterosCapInterface;
+import org.openhab.binding.mikrotik.internal.model.RouterosDevice;
+import org.openhab.binding.mikrotik.internal.model.RouterosEthernetInterface;
+import org.openhab.binding.mikrotik.internal.model.RouterosInterfaceBase;
+import org.openhab.binding.mikrotik.internal.model.RouterosL2TPCliInterface;
+import org.openhab.binding.mikrotik.internal.model.RouterosL2TPSrvInterface;
+import org.openhab.binding.mikrotik.internal.model.RouterosPPPoECliInterface;
+import org.openhab.binding.mikrotik.internal.model.RouterosWlanInterface;
+import org.openhab.binding.mikrotik.internal.util.RateCalculator;
+import org.openhab.binding.mikrotik.internal.util.StateUtil;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MikrotikInterfaceThingHandler} is a {@link MikrotikBaseThingHandler} subclass that wraps shared
+ * functionality for all interface things of different types. It is responsible for handling commands, which are
+ * sent to one of the channels and emit channel updates whenever required.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<InterfaceThingConfig> {
+    private final Logger logger = LoggerFactory.getLogger(MikrotikInterfaceThingHandler.class);
+
+    private @Nullable RouterosInterfaceBase iface;
+
+    private final RateCalculator txByteRate = new RateCalculator(BigDecimal.ZERO);
+    private final RateCalculator rxByteRate = new RateCalculator(BigDecimal.ZERO);
+    private final RateCalculator txPacketRate = new RateCalculator(BigDecimal.ZERO);
+    private final RateCalculator rxPacketRate = new RateCalculator(BigDecimal.ZERO);
+
+    public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return MikrotikBindingConstants.THING_TYPE_INTERFACE.equals(thingTypeUID);
+    }
+
+    public MikrotikInterfaceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+        RouterosDevice routeros = getRouterOs();
+        InterfaceThingConfig cfg = this.config;
+        if (routeros != null && cfg != null) {
+            if (status == ONLINE || (status == OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR)) {
+                routeros.registerForMonitoring(cfg.name);
+            } else if (status == OFFLINE
+                    && (statusDetail == ThingStatusDetail.CONFIGURATION_ERROR || statusDetail == GONE)) {
+                routeros.unregisterForMonitoring(cfg.name);
+            }
+        }
+        super.updateStatus(status, statusDetail, description);
+    }
+
+    @Override
+    protected void refreshModels() {
+        RouterosDevice routeros = getRouterOs();
+        InterfaceThingConfig cfg = this.config;
+        if (routeros != null && cfg != null) {
+            RouterosInterfaceBase rosInterface = routeros.findInterface(cfg.name);
+            this.iface = rosInterface;
+            if (rosInterface == null) {
+                String statusMsg = String.format("Interface %s is not found in RouterOS for thing %s", cfg.name,
+                        getThing().getUID());
+                updateStatus(OFFLINE, GONE, statusMsg);
+            } else {
+                txByteRate.update(rosInterface.getTxBytes());
+                rxByteRate.update(rosInterface.getRxBytes());
+                txPacketRate.update(rosInterface.getTxPackets());
+                rxPacketRate.update(rosInterface.getRxPackets());
+            }
+        }
+    }
+
+    @Override
+    protected void refreshChannel(ChannelUID channelUID) {
+        String channelID = channelUID.getIdWithoutGroup();
+        State oldState = currentState.getOrDefault(channelID, UnDefType.NULL);
+        State newState = oldState;
+        RouterosInterfaceBase iface = this.iface;
+        if (iface == null) {
+            newState = UnDefType.NULL;
+        } else {
+            switch (channelID) {
+                case MikrotikBindingConstants.CHANNEL_NAME:
+                    newState = StateUtil.stringOrNull(iface.getName());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_COMMENT:
+                    newState = StateUtil.stringOrNull(iface.getComment());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TYPE:
+                    newState = StateUtil.stringOrNull(iface.getType());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_MAC:
+                    newState = StateUtil.stringOrNull(iface.getMacAddress());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_ENABLED:
+                    newState = StateUtil.boolOrNull(iface.isEnabled());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_CONNECTED:
+                    newState = StateUtil.boolOrNull(iface.isConnected());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_LAST_LINK_DOWN_TIME:
+                    newState = StateUtil.timeOrNull(iface.getLastLinkDownTime());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_LAST_LINK_UP_TIME:
+                    newState = StateUtil.timeOrNull(iface.getLastLinkUpTime());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_LINK_DOWNS:
+                    newState = StateUtil.intOrNull(iface.getLinkDowns());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TX_DATA_RATE:
+                    newState = StateUtil.qtyMegabitPerSecOrNull(txByteRate.getMegabitRate());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_RX_DATA_RATE:
+                    newState = StateUtil.qtyMegabitPerSecOrNull(rxByteRate.getMegabitRate());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TX_PACKET_RATE:
+                    newState = StateUtil.floatOrNull(txPacketRate.getRate());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_RX_PACKET_RATE:
+                    newState = StateUtil.floatOrNull(rxPacketRate.getRate());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TX_BYTES:
+                    newState = StateUtil.bigIntOrNull(iface.getTxBytes());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_RX_BYTES:
+                    newState = StateUtil.bigIntOrNull(iface.getRxBytes());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TX_PACKETS:
+                    newState = StateUtil.bigIntOrNull(iface.getTxPackets());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_RX_PACKETS:
+                    newState = StateUtil.bigIntOrNull(iface.getRxPackets());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TX_DROPS:
+                    newState = StateUtil.bigIntOrNull(iface.getTxDrops());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_RX_DROPS:
+                    newState = StateUtil.bigIntOrNull(iface.getRxDrops());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TX_ERRORS:
+                    newState = StateUtil.bigIntOrNull(iface.getTxErrors());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_RX_ERRORS:
+                    newState = StateUtil.bigIntOrNull(iface.getRxErrors());
+                    break;
+                default:
+                    if (iface instanceof RouterosEthernetInterface) {
+                        newState = getEtherIterfaceChannelState(channelID);
+                    } else if (iface instanceof RouterosCapInterface) {
+                        newState = getCapIterfaceChannelState(channelID);
+                    } else if (iface instanceof RouterosWlanInterface) {
+                        newState = getWlanIterfaceChannelState(channelID);
+                    } else if (iface instanceof RouterosPPPoECliInterface) {
+                        newState = getPPPoECliChannelState(channelID);
+                    } else if (iface instanceof RouterosL2TPSrvInterface) {
+                        newState = getL2TPSrvChannelState(channelID);
+                    } else if (iface instanceof RouterosL2TPCliInterface) {
+                        newState = getL2TPCliChannelState(channelID);
+                    }
+            }
+        }
+
+        if (!newState.equals(oldState)) {
+            updateState(channelID, newState);
+            currentState.put(channelID, newState);
+        }
+    }
+
+    protected State getEtherIterfaceChannelState(String channelID) {
+        RouterosEthernetInterface etherIface = (RouterosEthernetInterface) this.iface;
+        if (etherIface == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (channelID) {
+            case MikrotikBindingConstants.CHANNEL_DEFAULT_NAME:
+                return StateUtil.stringOrNull(etherIface.getDefaultName());
+            case MikrotikBindingConstants.CHANNEL_STATE:
+                return StateUtil.stringOrNull(etherIface.getState());
+            case MikrotikBindingConstants.CHANNEL_RATE:
+                return StateUtil.stringOrNull(etherIface.getRate());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    protected State getCapIterfaceChannelState(String channelID) {
+        RouterosCapInterface capIface = (RouterosCapInterface) this.iface;
+        if (capIface == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (channelID) {
+            case MikrotikBindingConstants.CHANNEL_STATE:
+                return StateUtil.stringOrNull(capIface.getCurrentState());
+            case MikrotikBindingConstants.CHANNEL_RATE:
+                return StateUtil.stringOrNull(capIface.getRateSet());
+            case MikrotikBindingConstants.CHANNEL_REGISTERED_CLIENTS:
+                return StateUtil.intOrNull(capIface.getRegisteredClients());
+            case MikrotikBindingConstants.CHANNEL_AUTHORIZED_CLIENTS:
+                return StateUtil.intOrNull(capIface.getAuthorizedClients());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    protected State getWlanIterfaceChannelState(String channelID) {
+        RouterosWlanInterface wlIface = (RouterosWlanInterface) this.iface;
+        if (wlIface == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (channelID) {
+            case MikrotikBindingConstants.CHANNEL_STATE:
+                return StateUtil.stringOrNull(wlIface.getCurrentState());
+            case MikrotikBindingConstants.CHANNEL_RATE:
+                return StateUtil.stringOrNull(wlIface.getRate());
+            case MikrotikBindingConstants.CHANNEL_REGISTERED_CLIENTS:
+                return StateUtil.intOrNull(wlIface.getRegisteredClients());
+            case MikrotikBindingConstants.CHANNEL_AUTHORIZED_CLIENTS:
+                return StateUtil.intOrNull(wlIface.getAuthorizedClients());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    protected State getPPPoECliChannelState(String channelID) {
+        RouterosPPPoECliInterface pppCli = (RouterosPPPoECliInterface) this.iface;
+        if (pppCli == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (channelID) {
+            case MikrotikBindingConstants.CHANNEL_STATE:
+                return StateUtil.stringOrNull(pppCli.getStatus());
+            case MikrotikBindingConstants.CHANNEL_UP_SINCE:
+                return StateUtil.timeOrNull(pppCli.getUptimeStart());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    protected State getL2TPSrvChannelState(String channelID) {
+        RouterosL2TPSrvInterface vpnSrv = (RouterosL2TPSrvInterface) this.iface;
+        if (vpnSrv == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (channelID) {
+            case MikrotikBindingConstants.CHANNEL_STATE:
+                return StateUtil.stringOrNull(vpnSrv.getEncoding());
+            case MikrotikBindingConstants.CHANNEL_UP_SINCE:
+                return StateUtil.timeOrNull(vpnSrv.getUptimeStart());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    protected State getL2TPCliChannelState(String channelID) {
+        RouterosL2TPCliInterface vpnCli = (RouterosL2TPCliInterface) this.iface;
+        if (vpnCli == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (channelID) {
+            case MikrotikBindingConstants.CHANNEL_STATE:
+                return StateUtil.stringOrNull(vpnCli.getEncoding());
+            case MikrotikBindingConstants.CHANNEL_UP_SINCE:
+                return StateUtil.timeOrNull(vpnCli.getUptimeStart());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    @Override
+    protected void executeCommand(ChannelUID channelUID, Command command) {
+        if (iface == null) {
+            return;
+        }
+        logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikRouterosBridgeHandler.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikRouterosBridgeHandler.java
new file mode 100644 (file)
index 0000000..e31675e
--- /dev/null
@@ -0,0 +1,292 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.handler;
+
+import static org.openhab.core.thing.ThingStatus.ONLINE;
+import static org.openhab.core.types.RefreshType.REFRESH;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.MikrotikBindingConstants;
+import org.openhab.binding.mikrotik.internal.config.RouterosThingConfig;
+import org.openhab.binding.mikrotik.internal.model.RouterosDevice;
+import org.openhab.binding.mikrotik.internal.model.RouterosRouterboardInfo;
+import org.openhab.binding.mikrotik.internal.model.RouterosSystemResources;
+import org.openhab.binding.mikrotik.internal.util.StateUtil;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import me.legrange.mikrotik.MikrotikApiException;
+
+/**
+ * The {@link MikrotikRouterosBridgeHandler} is a main binding class that wraps a {@link RouterosDevice} and
+ * manages fetching data from RouterOS. It is also responsible for updating brindge thing properties and
+ * handling commands, which are sent to one of the channels and emit channel updates whenever required.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
+    private final Logger logger = LoggerFactory.getLogger(MikrotikRouterosBridgeHandler.class);
+    private @Nullable RouterosThingConfig config;
+    private @Nullable volatile RouterosDevice routeros;
+    private @Nullable ScheduledFuture<?> refreshJob;
+    private Map<String, State> currentState = new HashMap<>();
+
+    public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return MikrotikBindingConstants.THING_TYPE_ROUTEROS.equals(thingTypeUID);
+    }
+
+    public MikrotikRouterosBridgeHandler(Bridge bridge) {
+        super(bridge);
+    }
+
+    @Override
+    public void initialize() {
+        cancelRefreshJob();
+        var cfg = getConfigAs(RouterosThingConfig.class);
+        this.config = cfg;
+        logger.debug("Initializing MikrotikRouterosBridgeHandler with config = {}", cfg);
+        if (cfg.isValid()) {
+            this.routeros = new RouterosDevice(cfg.host, cfg.port, cfg.login, cfg.password);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, String.format("Connecting to %s", cfg.host));
+            scheduleRefreshJob();
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration is not valid");
+        }
+    }
+
+    public @Nullable RouterosDevice getRouteros() {
+        return routeros;
+    }
+
+    public @Nullable RouterosThingConfig getBridgeConfig() {
+        return config;
+    }
+
+    @Override
+    protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+        if (status == ThingStatus.ONLINE
+                || (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR)) {
+            scheduleRefreshJob();
+        } else if (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.CONFIGURATION_ERROR) {
+            cancelRefreshJob();
+        }
+        // update the status only if it's changed
+        ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
+                .build();
+        if (!statusInfo.equals(getThing().getStatusInfo())) {
+            super.updateStatus(status, statusDetail, description);
+        }
+    }
+
+    @Override
+    public void dispose() {
+        cancelRefreshJob();
+        var routeros = this.routeros;
+        if (routeros != null) {
+            routeros.stop();
+            this.routeros = null;
+        }
+    }
+
+    private void scheduleRefreshJob() {
+        synchronized (this) {
+            var cfg = this.config;
+            if (refreshJob == null) {
+                int refreshPeriod = 10;
+                if (cfg != null) {
+                    refreshPeriod = cfg.refresh;
+                } else {
+                    logger.warn("null config spotted in scheduleRefreshJob");
+                }
+                logger.debug("Scheduling refresh job every {}s", refreshPeriod);
+                refreshJob = scheduler.scheduleWithFixedDelay(this::scheduledRun, 0, refreshPeriod, TimeUnit.SECONDS);
+            }
+        }
+    }
+
+    private void cancelRefreshJob() {
+        synchronized (this) {
+            var job = this.refreshJob;
+            if (job != null) {
+                logger.debug("Cancelling refresh job");
+                job.cancel(true);
+                this.refreshJob = null;
+            }
+        }
+    }
+
+    private void scheduledRun() {
+        var routeros = this.routeros;
+        if (routeros == null) {
+            logger.error("RouterOS device is null in scheduledRun");
+            return;
+        }
+        if (!routeros.isConnected()) {
+            // Perform connection
+            try {
+                logger.debug("Starting routeros model");
+                routeros.start();
+
+                RouterosRouterboardInfo rbInfo = routeros.getRouterboardInfo();
+                if (rbInfo != null) {
+                    Map<String, String> bridgeProps = editProperties();
+                    bridgeProps.put(MikrotikBindingConstants.PROPERTY_MODEL, rbInfo.getModel());
+                    bridgeProps.put(MikrotikBindingConstants.PROPERTY_FIRMWARE, rbInfo.getFirmware());
+                    bridgeProps.put(MikrotikBindingConstants.PROPERTY_SERIAL_NUMBER, rbInfo.getSerialNumber());
+                    updateProperties(bridgeProps);
+                } else {
+                    logger.warn("Failed to set RouterBOARD properties for bridge {}", getThing().getUID());
+                }
+                updateStatus(ThingStatus.ONLINE);
+            } catch (MikrotikApiException e) {
+                logger.warn("Error while logging in to RouterOS {} | Cause: {}", getThing().getUID(), e, e.getCause());
+
+                String errorMessage = e.getMessage();
+                if (errorMessage == null) {
+                    errorMessage = "Error connecting (UNKNOWN ERROR)";
+                }
+                if (errorMessage.contains("Command timed out") || errorMessage.contains("Error connecting")) {
+                    routeros.stop();
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
+                } else if (errorMessage.contains("Connection refused")) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                            "Remote host refused to connect, make sure port is correct");
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
+                }
+            }
+        } else {
+            // We're connected - do a usual polling cycle
+            performRefresh();
+        }
+    }
+
+    private void performRefresh() {
+        var routeros = this.routeros;
+        if (routeros == null) {
+            logger.error("RouterOS device is null in performRefresh");
+            return;
+        }
+        try {
+            logger.debug("Refreshing RouterOS caches for {}", getThing().getUID());
+            routeros.refresh();
+            // refresh own channels
+            for (Channel channel : getThing().getChannels()) {
+                try {
+                    refreshChannel(channel.getUID());
+                } catch (RuntimeException e) {
+                    throw new ChannelUpdateException(getThing().getUID(), channel.getUID(), e);
+                }
+            }
+            // refresh all the client things below
+            getThing().getThings().forEach(thing -> {
+                ThingHandler handler = thing.getHandler();
+                if (handler instanceof MikrotikBaseThingHandler<?>) {
+                    ((MikrotikBaseThingHandler<?>) handler).refresh();
+                }
+            });
+        } catch (ChannelUpdateException e) {
+            logger.debug("Error updating channel! {}", e.getMessage(), e.getCause());
+        } catch (MikrotikApiException e) {
+            logger.error("RouterOS cache refresh failed in {} due to Mikrotik API error", getThing().getUID(), e);
+            routeros.stop();
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        } catch (Exception e) {
+            logger.error("Unhandled exception while refreshing the {} RouterOS model", getThing().getUID(), e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("Handling command = {} for channel = {}", command, channelUID);
+        if (getThing().getStatus() == ONLINE) {
+            RouterosDevice routeros = getRouteros();
+            if (routeros != null) {
+                if (command == REFRESH) {
+                    refreshChannel(channelUID);
+                } else {
+                    logger.warn("Ignoring command = {} for channel = {} as it is not yet supported", command,
+                            channelUID);
+                }
+            }
+        }
+    }
+
+    protected void refreshChannel(ChannelUID channelUID) {
+        RouterosDevice routerOs = getRouteros();
+        String channelID = channelUID.getIdWithoutGroup();
+        RouterosSystemResources rbRes = null;
+        if (routerOs != null) {
+            rbRes = routerOs.getSysResources();
+        }
+        State oldState = currentState.getOrDefault(channelID, UnDefType.NULL);
+        State newState = oldState;
+
+        if (rbRes == null) {
+            newState = UnDefType.NULL;
+        } else {
+            switch (channelID) {
+                case MikrotikBindingConstants.CHANNEL_UP_SINCE:
+                    newState = StateUtil.timeOrNull(rbRes.getUptimeStart());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_FREE_SPACE:
+                    newState = StateUtil.qtyBytesOrNull(rbRes.getFreeSpace());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TOTAL_SPACE:
+                    newState = StateUtil.qtyBytesOrNull(rbRes.getTotalSpace());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_USED_SPACE:
+                    newState = StateUtil.qtyPercentOrNull(rbRes.getSpaceUse());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_FREE_MEM:
+                    newState = StateUtil.qtyBytesOrNull(rbRes.getFreeMem());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_TOTAL_MEM:
+                    newState = StateUtil.qtyBytesOrNull(rbRes.getTotalMem());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_USED_MEM:
+                    newState = StateUtil.qtyPercentOrNull(rbRes.getMemUse());
+                    break;
+                case MikrotikBindingConstants.CHANNEL_CPU_LOAD:
+                    newState = StateUtil.qtyPercentOrNull(rbRes.getCpuLoad());
+                    break;
+            }
+        }
+
+        if (!newState.equals(oldState)) {
+            updateState(channelID, newState);
+            currentState.put(channelID, newState);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikWirelessClientThingHandler.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikWirelessClientThingHandler.java
new file mode 100644 (file)
index 0000000..20aec9d
--- /dev/null
@@ -0,0 +1,239 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.handler;
+
+import static org.openhab.binding.mikrotik.internal.MikrotikBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.MikrotikBindingConstants;
+import org.openhab.binding.mikrotik.internal.config.WirelessClientThingConfig;
+import org.openhab.binding.mikrotik.internal.model.RouterosCapsmanRegistration;
+import org.openhab.binding.mikrotik.internal.model.RouterosDevice;
+import org.openhab.binding.mikrotik.internal.model.RouterosRegistrationBase;
+import org.openhab.binding.mikrotik.internal.model.RouterosWirelessRegistration;
+import org.openhab.binding.mikrotik.internal.util.RateCalculator;
+import org.openhab.binding.mikrotik.internal.util.StateUtil;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MikrotikWirelessClientThingHandler} is a {@link MikrotikBaseThingHandler} subclass that wraps shared
+ * functionality for all wireless clients listed either in CAPsMAN or Wireless RouterOS sections.
+ * It is responsible for handling commands, which are sent to one of the channels and emit channel updates whenever
+ * required.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class MikrotikWirelessClientThingHandler extends MikrotikBaseThingHandler<WirelessClientThingConfig> {
+    private final Logger logger = LoggerFactory.getLogger(MikrotikWirelessClientThingHandler.class);
+
+    private @Nullable RouterosRegistrationBase wifiReg;
+
+    private boolean online = false;
+    private boolean continuousConnection = false;
+    private @Nullable LocalDateTime lastSeen;
+
+    private final RateCalculator txByteRate = new RateCalculator(BigDecimal.ZERO);
+    private final RateCalculator rxByteRate = new RateCalculator(BigDecimal.ZERO);
+    private final RateCalculator txPacketRate = new RateCalculator(BigDecimal.ZERO);
+    private final RateCalculator rxPacketRate = new RateCalculator(BigDecimal.ZERO);
+
+    public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return MikrotikBindingConstants.THING_TYPE_WIRELESS_CLIENT.equals(thingTypeUID);
+    }
+
+    public MikrotikWirelessClientThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    private boolean fetchModels() {
+        var cfg = this.config;
+        if (cfg != null) {
+            RouterosDevice routeros = getRouterOs();
+
+            RouterosWirelessRegistration wifiRegistration = null;
+            if (routeros != null) {
+                wifiRegistration = routeros.findWirelessRegistration(cfg.mac);
+            }
+            this.wifiReg = wifiRegistration;
+            if (wifiRegistration != null && !cfg.ssid.isBlank()
+                    && !cfg.ssid.equalsIgnoreCase(wifiRegistration.getSSID())) {
+                this.wifiReg = null;
+            }
+
+            if (this.wifiReg == null && routeros != null) {
+                // try looking in capsman when there is no wirelessRegistration
+                RouterosCapsmanRegistration capsmanReg = routeros.findCapsmanRegistration(cfg.mac);
+                this.wifiReg = capsmanReg;
+                if (capsmanReg != null && !cfg.ssid.isBlank() && !cfg.ssid.equalsIgnoreCase(capsmanReg.getSSID())) {
+                    this.wifiReg = null;
+                }
+            }
+        }
+        return this.wifiReg != null;
+    }
+
+    @Override
+    protected void refreshModels() {
+        this.online = fetchModels();
+        if (online) {
+            lastSeen = LocalDateTime.now();
+        } else {
+            continuousConnection = false;
+        }
+        var wifiReg = this.wifiReg;
+        if (wifiReg != null) {
+            var cfg = this.config;
+            int considerContinuous = 180;
+            if (cfg != null) {
+                considerContinuous = cfg.considerContinuous;
+            }
+            txByteRate.update(wifiReg.getTxBytes());
+            rxByteRate.update(wifiReg.getRxBytes());
+            txPacketRate.update(wifiReg.getTxPackets());
+            rxPacketRate.update(wifiReg.getRxPackets());
+            LocalDateTime uptimeStart = wifiReg.getUptimeStart();
+            continuousConnection = (uptimeStart != null)
+                    && LocalDateTime.now().isAfter(uptimeStart.plusSeconds(considerContinuous));
+        }
+    }
+
+    @Override
+    protected void refreshChannel(ChannelUID channelUID) {
+        var wifiReg = this.wifiReg;
+        if (wifiReg == null) {
+            logger.warn("wifiReg is null in refreshChannel({})", channelUID);
+            return;
+        }
+
+        String channelID = channelUID.getIdWithoutGroup();
+        State oldState = currentState.getOrDefault(channelID, UnDefType.NULL);
+        State newState = oldState;
+
+        if (channelID.equals(CHANNEL_CONNECTED)) {
+            newState = StateUtil.boolOrNull(online);
+        } else if (channelID.equals(CHANNEL_LAST_SEEN)) {
+            newState = StateUtil.timeOrNull(lastSeen);
+        } else if (channelID.equals(CHANNEL_CONTINUOUS)) {
+            newState = StateUtil.boolOrNull(continuousConnection);
+        } else if (!online) {
+            newState = UnDefType.NULL;
+        } else {
+            switch (channelID) {
+                case CHANNEL_NAME:
+                    newState = StateUtil.stringOrNull(wifiReg.getName());
+                    break;
+                case CHANNEL_COMMENT:
+                    newState = StateUtil.stringOrNull(wifiReg.getComment());
+                    break;
+                case CHANNEL_MAC:
+                    newState = StateUtil.stringOrNull(wifiReg.getMacAddress());
+                    break;
+                case CHANNEL_INTERFACE:
+                    newState = StateUtil.stringOrNull(wifiReg.getInterfaceName());
+                    break;
+                case CHANNEL_SSID:
+                    newState = StateUtil.stringOrNull(wifiReg.getSSID());
+                    break;
+                case CHANNEL_UP_SINCE:
+                    newState = StateUtil.timeOrNull(wifiReg.getUptimeStart());
+                    break;
+                case CHANNEL_TX_DATA_RATE:
+                    newState = StateUtil.qtyMegabitPerSecOrNull(txByteRate.getMegabitRate());
+                    break;
+                case CHANNEL_RX_DATA_RATE:
+                    newState = StateUtil.qtyMegabitPerSecOrNull(rxByteRate.getMegabitRate());
+                    break;
+                case CHANNEL_TX_PACKET_RATE:
+                    newState = StateUtil.floatOrNull(txPacketRate.getRate());
+                    break;
+                case CHANNEL_RX_PACKET_RATE:
+                    newState = StateUtil.floatOrNull(rxPacketRate.getRate());
+                    break;
+                case CHANNEL_TX_BYTES:
+                    newState = StateUtil.bigIntOrNull(wifiReg.getTxBytes());
+                    break;
+                case CHANNEL_RX_BYTES:
+                    newState = StateUtil.bigIntOrNull(wifiReg.getRxBytes());
+                    break;
+                case CHANNEL_TX_PACKETS:
+                    newState = StateUtil.bigIntOrNull(wifiReg.getTxPackets());
+                    break;
+                case CHANNEL_RX_PACKETS:
+                    newState = StateUtil.bigIntOrNull(wifiReg.getRxPackets());
+                    break;
+                default:
+                    newState = UnDefType.NULL;
+                    if (wifiReg instanceof RouterosWirelessRegistration) {
+                        newState = getWirelessRegistrationChannelState(channelID);
+                    } else if (wifiReg instanceof RouterosCapsmanRegistration) {
+                        newState = getCapsmanRegistrationChannelState(channelID);
+                    }
+            }
+        }
+
+        if (!newState.equals(oldState)) {
+            updateState(channelID, newState);
+            currentState.put(channelID, newState);
+        }
+    }
+
+    @SuppressWarnings("null")
+    protected State getCapsmanRegistrationChannelState(String channelID) {
+        if (this.wifiReg == null) {
+            return UnDefType.UNDEF;
+        }
+
+        RouterosCapsmanRegistration capsmanReg = (RouterosCapsmanRegistration) this.wifiReg;
+        switch (channelID) {
+            case CHANNEL_SIGNAL:
+                return StateUtil.intOrNull(capsmanReg.getRxSignal());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    @SuppressWarnings("null")
+    protected State getWirelessRegistrationChannelState(String channelID) {
+        if (this.wifiReg == null) {
+            return UnDefType.UNDEF;
+        }
+
+        RouterosWirelessRegistration wirelessReg = (RouterosWirelessRegistration) this.wifiReg;
+        switch (channelID) {
+            case CHANNEL_SIGNAL:
+                return StateUtil.intOrNull(wirelessReg.getRxSignal());
+            default:
+                return UnDefType.UNDEF;
+        }
+    }
+
+    @Override
+    protected void executeCommand(ChannelUID channelUID, Command command) {
+        if (!online) {
+            return;
+        }
+        logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBaseData.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBaseData.java
new file mode 100644 (file)
index 0000000..3dd9a84
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.math.BigInteger;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link RouterosBaseData} is a base class for other data models having internal hashmap access methods and
+ * values convertors.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public abstract class RouterosBaseData {
+    private final Map<String, String> propMap;
+
+    public RouterosBaseData(Map<String, String> props) {
+        this.propMap = props;
+    }
+
+    public void mergeProps(Map<String, String> otherProps) {
+        this.propMap.putAll(otherProps);
+    }
+
+    protected boolean hasProp(String key) {
+        return propMap.containsKey(key);
+    }
+
+    protected String getProp(String key, String defaultValue) {
+        return propMap.getOrDefault(key, defaultValue);
+    }
+
+    protected void setProp(String key, String value) {
+        propMap.put(key, value);
+    }
+
+    protected @Nullable String getProp(String key) {
+        return propMap.get(key);
+    }
+
+    protected @Nullable Integer getIntProp(String key) {
+        String val = propMap.get(key);
+        return val == null ? null : Integer.valueOf(val);
+    }
+
+    protected @Nullable BigInteger getBigIntProp(String key) {
+        String val = propMap.get(key);
+        return val == null ? null : new BigInteger(propMap.getOrDefault(key, "0"));
+    }
+
+    protected @Nullable Float getFloatProp(String key) {
+        String val = propMap.get(key);
+        return val == null ? null : Float.valueOf(val);
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBridgeInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBridgeInterface.java
new file mode 100644 (file)
index 0000000..49ba800
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RouterosBridgeInterface} is a model class for `bridge` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosBridgeInterface extends RouterosInterfaceBase {
+    public RouterosBridgeInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.BRIDGE;
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return false;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapInterface.java
new file mode 100644 (file)
index 0000000..0be4e89
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link RouterosCapInterface} is a model class for `cap` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosCapInterface extends RouterosInterfaceBase {
+    public RouterosCapInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.CAP;
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return true;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        return false;
+    }
+
+    public boolean isMaster() {
+        return getProp("slave", "").equals("false");
+    }
+
+    public boolean isDynamic() {
+        return getProp("dynamic", "").equals("true");
+    }
+
+    public boolean isBound() {
+        return getProp("bound", "").equals("true");
+    }
+
+    public boolean isActive() {
+        return getProp("inactive", "").equals("false");
+    }
+
+    public @Nullable String getCurrentState() {
+        return getProp("current-state");
+    }
+
+    public @Nullable String getRateSet() {
+        return getProp("current-basic-rate-set");
+    }
+
+    public @Nullable Integer getRegisteredClients() {
+        return getIntProp("current-registered-clients");
+    }
+
+    public @Nullable Integer getAuthorizedClients() {
+        return getIntProp("current-authorized-clients");
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapsmanRegistration.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapsmanRegistration.java
new file mode 100644 (file)
index 0000000..4d3c200
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link RouterosCapsmanRegistration} is a model class for WiFi client data retrieced from CAPsMAN controller
+ * in RouterOS. Is a subclass of {@link RouterosRegistrationBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosCapsmanRegistration extends RouterosRegistrationBase {
+    public RouterosCapsmanRegistration(Map<String, String> props) {
+        super(props);
+    }
+
+    public @Nullable String getIdentity() {
+        return getProp("eap-identity");
+    }
+
+    public @Nullable Integer getRxSignal() {
+        return getIntProp("rx-signal");
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosDevice.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosDevice.java
new file mode 100644 (file)
index 0000000..4736132
--- /dev/null
@@ -0,0 +1,301 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.net.SocketFactory;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import me.legrange.mikrotik.ApiConnection;
+import me.legrange.mikrotik.ApiConnectionException;
+import me.legrange.mikrotik.MikrotikApiException;
+
+/**
+ * The {@link RouterosDevice} class is wrapped inside a bridge thing and responsible for communication with
+ * Mikrotik device, data fetching, caching and aggregation.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosDevice {
+    private final Logger logger = LoggerFactory.getLogger(RouterosDevice.class);
+
+    private final String host;
+    private final int port;
+    private final int connectionTimeout;
+    private final String login;
+    private final String password;
+    private @Nullable ApiConnection connection;
+
+    public static final String PROP_ID_KEY = ".id";
+    public static final String PROP_TYPE_KEY = "type";
+    public static final String PROP_NAME_KEY = "name";
+    public static final String PROP_SSID_KEY = "ssid";
+
+    private static final String CMD_PRINT_IFACES = "/interface/print";
+    private static final String CMD_PRINT_IFACE_TYPE_TPL = "/interface/%s/print";
+    private static final String CMD_MONTOR_IFACE_MONITOR_TPL = "/interface/%s/monitor numbers=%s once";
+    private static final String CMD_PRINT_CAPS_IFACES = "/caps-man/interface/print";
+    private static final String CMD_PRINT_CAPSMAN_REGS = "/caps-man/registration-table/print";
+    private static final String CMD_PRINT_WIRELESS_REGS = "/interface/wireless/registration-table/print";
+    private static final String CMD_PRINT_RESOURCE = "/system/resource/print";
+    private static final String CMD_PRINT_RB_INFO = "/system/routerboard/print";
+
+    private final List<RouterosInterfaceBase> interfaceCache = new ArrayList<>();
+    private final List<RouterosCapsmanRegistration> capsmanRegistrationCache = new ArrayList<>();
+    private final List<RouterosWirelessRegistration> wirelessRegistrationCache = new ArrayList<>();
+    private final Set<String> monitoredInterfaces = new HashSet<>();
+    private final Map<String, String> wlanSsid = new HashMap<>();
+
+    private @Nullable RouterosSystemResources resourcesCache;
+    private @Nullable RouterosRouterboardInfo rbInfo;
+
+    private static Optional<RouterosInterfaceBase> createTypedInterface(Map<String, String> interfaceProps) {
+        RouterosInterfaceType ifaceType = RouterosInterfaceType.resolve(interfaceProps.get(PROP_TYPE_KEY));
+        if (ifaceType == null) {
+            return Optional.empty();
+        }
+        switch (ifaceType) {
+            case ETHERNET:
+                return Optional.of(new RouterosEthernetInterface(interfaceProps));
+            case BRIDGE:
+                return Optional.of(new RouterosBridgeInterface(interfaceProps));
+            case CAP:
+                return Optional.of(new RouterosCapInterface(interfaceProps));
+            case WLAN:
+                return Optional.of(new RouterosWlanInterface(interfaceProps));
+            case PPPOE_CLIENT:
+                return Optional.of(new RouterosPPPoECliInterface(interfaceProps));
+            case L2TP_SERVER:
+                return Optional.of(new RouterosL2TPSrvInterface(interfaceProps));
+            case L2TP_CLIENT:
+                return Optional.of(new RouterosL2TPCliInterface(interfaceProps));
+            default:
+                return Optional.empty();
+        }
+    }
+
+    public RouterosDevice(String host, int port, String login, String password) {
+        this.host = host;
+        this.port = port;
+        this.login = login;
+        this.password = password;
+        this.connectionTimeout = ApiConnection.DEFAULT_CONNECTION_TIMEOUT;
+    }
+
+    public boolean isConnected() {
+        ApiConnection conn = this.connection;
+        return conn != null && conn.isConnected();
+    }
+
+    public void start() throws MikrotikApiException {
+        login();
+        updateRouterboardInfo();
+    }
+
+    public void stop() {
+        ApiConnection conn = this.connection;
+        if (conn != null && conn.isConnected()) {
+            logout();
+        }
+    }
+
+    public void login() throws MikrotikApiException {
+        logger.debug("Attempting login to {} ...", host);
+        ApiConnection conn = ApiConnection.connect(SocketFactory.getDefault(), host, port, connectionTimeout);
+        conn.login(login, password);
+        logger.debug("Logged in to RouterOS at {} !", host);
+        this.connection = conn;
+    }
+
+    public void logout() {
+        ApiConnection conn = this.connection;
+        logger.debug("Logging out of {}", host);
+        if (conn != null) {
+            logger.debug("Closing connection to {}", host);
+            try {
+                conn.close();
+            } catch (ApiConnectionException e) {
+                logger.debug("Logout error", e);
+            } finally {
+                this.connection = null;
+            }
+        }
+    }
+
+    public boolean registerForMonitoring(String interfaceName) {
+        return monitoredInterfaces.add(interfaceName);
+    }
+
+    public boolean unregisterForMonitoring(String interfaceName) {
+        return monitoredInterfaces.remove(interfaceName);
+    }
+
+    public void refresh() throws MikrotikApiException {
+        synchronized (this) {
+            updateResources();
+            updateInterfaceData();
+            updateCapsmanRegistrations();
+            updateWirelessRegistrations();
+        }
+    }
+
+    public @Nullable RouterosRouterboardInfo getRouterboardInfo() {
+        return rbInfo;
+    }
+
+    public @Nullable RouterosSystemResources getSysResources() {
+        return resourcesCache;
+    }
+
+    public @Nullable RouterosCapsmanRegistration findCapsmanRegistration(String macAddress) {
+        Optional<RouterosCapsmanRegistration> searchResult = capsmanRegistrationCache.stream()
+                .filter(registration -> macAddress.equalsIgnoreCase(registration.getMacAddress())).findFirst();
+        return searchResult.orElse(null);
+    }
+
+    public @Nullable RouterosWirelessRegistration findWirelessRegistration(String macAddress) {
+        Optional<RouterosWirelessRegistration> searchResult = wirelessRegistrationCache.stream()
+                .filter(registration -> macAddress.equalsIgnoreCase(registration.getMacAddress())).findFirst();
+        return searchResult.orElse(null);
+    }
+
+    @SuppressWarnings("null")
+    public @Nullable RouterosInterfaceBase findInterface(String name) {
+        Optional<RouterosInterfaceBase> searchResult = interfaceCache.stream()
+                .filter(iface -> iface.getName() != null && iface.getName().equalsIgnoreCase(name)).findFirst();
+        return searchResult.orElse(null);
+    }
+
+    @SuppressWarnings("null")
+    private void updateInterfaceData() throws MikrotikApiException {
+        ApiConnection conn = this.connection;
+        if (conn == null) {
+            return;
+        }
+
+        List<Map<String, String>> ifaceResponse = conn.execute(CMD_PRINT_IFACES);
+
+        Set<String> interfaceTypesToPoll = new HashSet<>();
+        this.wlanSsid.clear();
+        this.interfaceCache.clear();
+        ifaceResponse.forEach(props -> {
+            Optional<RouterosInterfaceBase> ifaceOpt = createTypedInterface(props);
+            if (ifaceOpt.isPresent()) {
+                RouterosInterfaceBase iface = ifaceOpt.get();
+                if (iface.hasDetailedReport()) {
+                    interfaceTypesToPoll.add(iface.getApiType());
+                }
+                this.interfaceCache.add(iface);
+            }
+        });
+
+        Map<String, Map<String, String>> typedIfaceResponse = new HashMap<>();
+        for (String ifaceApiType : interfaceTypesToPoll) {
+            String cmd = String.format(CMD_PRINT_IFACE_TYPE_TPL, ifaceApiType);
+            if (ifaceApiType.compareTo("cap") == 0) {
+                cmd = CMD_PRINT_CAPS_IFACES;
+            }
+            connection.execute(cmd).forEach(propMap -> {
+                String ifaceName = propMap.get(PROP_NAME_KEY);
+                if (ifaceName != null) {
+                    if (typedIfaceResponse.containsKey(ifaceName)) {
+                        typedIfaceResponse.get(ifaceName).putAll(propMap);
+                    } else {
+                        typedIfaceResponse.put(ifaceName, propMap);
+                    }
+                }
+            });
+        }
+
+        for (RouterosInterfaceBase ifaceModel : interfaceCache) {
+            // Enrich with detailed data
+
+            Map<String, String> additionalIfaceProps = typedIfaceResponse.get(ifaceModel.getName());
+            if (additionalIfaceProps != null) {
+                ifaceModel.mergeProps(additionalIfaceProps);
+            }
+            // Get monitor data
+            if (ifaceModel.hasMonitor() && monitoredInterfaces.contains(ifaceModel.getName())) {
+                String cmd = String.format(CMD_MONTOR_IFACE_MONITOR_TPL, ifaceModel.getApiType(), ifaceModel.getName());
+                List<Map<String, String>> monitorProps = connection.execute(cmd);
+                ifaceModel.mergeProps(monitorProps.get(0));
+            }
+            // Note SSIDs for non-CAPsMAN wireless clients
+            String ifaceName = ifaceModel.getName();
+            String ifaceSsid = ifaceModel.getProperty(PROP_SSID_KEY);
+            if (ifaceName != null && ifaceSsid != null && !ifaceName.isBlank() && !ifaceSsid.isBlank()) {
+                this.wlanSsid.put(ifaceName, ifaceSsid);
+            }
+        }
+    }
+
+    private void updateCapsmanRegistrations() throws MikrotikApiException {
+        ApiConnection conn = this.connection;
+        if (conn == null) {
+            return;
+        }
+        List<Map<String, String>> response = conn.execute(CMD_PRINT_CAPSMAN_REGS);
+        if (response != null) {
+            capsmanRegistrationCache.clear();
+            response.forEach(reg -> capsmanRegistrationCache.add(new RouterosCapsmanRegistration(reg)));
+        }
+    }
+
+    private void updateWirelessRegistrations() throws MikrotikApiException {
+        ApiConnection conn = this.connection;
+        if (conn == null) {
+            return;
+        }
+        List<Map<String, String>> response = conn.execute(CMD_PRINT_WIRELESS_REGS);
+        wirelessRegistrationCache.clear();
+        response.forEach(props -> {
+            String wlanIfaceName = props.get("interface");
+            String wlanSsidName = wlanSsid.get(wlanIfaceName);
+
+            if (wlanSsidName != null && wlanIfaceName != null && !wlanIfaceName.isBlank() && !wlanSsidName.isBlank()) {
+                props.put(PROP_SSID_KEY, wlanSsidName);
+            }
+            wirelessRegistrationCache.add(new RouterosWirelessRegistration(props));
+        });
+    }
+
+    private void updateResources() throws MikrotikApiException {
+        ApiConnection conn = this.connection;
+        if (conn == null) {
+            return;
+        }
+        List<Map<String, String>> response = conn.execute(CMD_PRINT_RESOURCE);
+        this.resourcesCache = new RouterosSystemResources(response.get(0));
+    }
+
+    private void updateRouterboardInfo() throws MikrotikApiException {
+        ApiConnection conn = this.connection;
+        if (conn == null) {
+            return;
+        }
+        List<Map<String, String>> response = conn.execute(CMD_PRINT_RB_INFO);
+        this.rbInfo = new RouterosRouterboardInfo(response.get(0));
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosEthernetInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosEthernetInterface.java
new file mode 100644 (file)
index 0000000..ec12087
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link RouterosEthernetInterface} is a model class for `ether` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosEthernetInterface extends RouterosInterfaceBase {
+    public RouterosEthernetInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.ETHERNET;
+    }
+
+    @Override
+    public String getApiType() {
+        return "ethernet";
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return true;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        // PowerLine interfaces are of ehter type too
+        String name = getDefaultName();
+        return name != null && !name.startsWith("pwr");
+    }
+
+    public @Nullable String getDefaultName() {
+        return getProp("default-name");
+    }
+
+    public @Nullable String getRate() {
+        return getProp("rate");
+    }
+
+    public @Nullable String getState() {
+        return getProp("status");
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosInterfaceBase.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosInterfaceBase.java
new file mode 100644 (file)
index 0000000..f9ce780
--- /dev/null
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import static org.openhab.binding.mikrotik.internal.model.RouterosDevice.PROP_ID_KEY;
+
+import java.math.BigInteger;
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosInterfaceBase} is a base model class for network interface models having casting accessors for
+ * data that is same for all interface types.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public abstract class RouterosInterfaceBase extends RouterosBaseData {
+    protected @Nullable RouterosInterfaceType type;
+
+    public RouterosInterfaceBase(Map<String, String> props) {
+        super(props);
+        this.type = RouterosInterfaceType.resolve(getType());
+    }
+
+    public @Nullable String getProperty(String propName) {
+        return getProp(propName);
+    }
+
+    public abstract RouterosInterfaceType getDesignedType();
+
+    public abstract boolean hasDetailedReport();
+
+    public abstract boolean hasMonitor();
+
+    public String getApiType() {
+        return getDesignedType().toString();
+    };
+
+    public boolean validate() {
+        return getDesignedType() == this.type;
+    }
+
+    public @Nullable String getId() {
+        return getProp(PROP_ID_KEY);
+    }
+
+    public @Nullable String getType() {
+        return getProp("type");
+    }
+
+    public @Nullable String getName() {
+        return getProp("name");
+    }
+
+    public @Nullable String getComment() {
+        return getProp("comment");
+    }
+
+    public @Nullable String getMacAddress() {
+        return getProp("mac-address");
+    }
+
+    public boolean isEnabled() {
+        return getProp("disabled", "").equals("false");
+    }
+
+    public boolean isConnected() {
+        return getProp("running", "").equals("true");
+    }
+
+    public @Nullable Integer getLinkDowns() {
+        return getIntProp("link-downs");
+    }
+
+    public @Nullable LocalDateTime getLastLinkDownTime() {
+        return Converter.fromRouterosTime(getProp("last-link-down-time"));
+    }
+
+    public @Nullable LocalDateTime getLastLinkUpTime() {
+        return Converter.fromRouterosTime(getProp("last-link-up-time"));
+    }
+
+    public @Nullable BigInteger getTxBytes() {
+        return getBigIntProp("tx-byte");
+    }
+
+    public @Nullable BigInteger getRxBytes() {
+        return getBigIntProp("rx-byte");
+    }
+
+    public @Nullable BigInteger getTxPackets() {
+        return getBigIntProp("tx-packet");
+    }
+
+    public @Nullable BigInteger getRxPackets() {
+        return getBigIntProp("rx-packet");
+    }
+
+    public @Nullable BigInteger getTxDrops() {
+        return getBigIntProp("tx-drop");
+    }
+
+    public @Nullable BigInteger getRxDrops() {
+        return getBigIntProp("rx-drop");
+    }
+
+    public @Nullable BigInteger getTxErrors() {
+        return getBigIntProp("tx-error");
+    }
+
+    public @Nullable BigInteger getRxErrors() {
+        return getBigIntProp("rx-error");
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosInterfaceType.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosInterfaceType.java
new file mode 100644 (file)
index 0000000..26d89be
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link RouterosInterfaceType} enum wraps RouterOS network interface type strings.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+
+@NonNullByDefault
+public enum RouterosInterfaceType {
+    ETHERNET("ether"),
+    BRIDGE("bridge"),
+    WLAN("wlan"),
+    CAP("cap"),
+    PPPOE_CLIENT("pppoe-out"),
+    L2TP_SERVER("l2tp-in"),
+    L2TP_CLIENT("l2tp-out");
+
+    private final String typeName;
+
+    RouterosInterfaceType(String routerosType) {
+        this.typeName = routerosType;
+    }
+
+    public boolean equalsName(String otherType) {
+        // (otherName == null) check is not needed because name.equals(null) returns false
+        return typeName.equals(otherType);
+    }
+
+    public String toString() {
+        return this.typeName;
+    }
+
+    public @Nullable static RouterosInterfaceType resolve(@Nullable String routerosType) {
+        for (RouterosInterfaceType current : RouterosInterfaceType.values()) {
+            if (current.typeName.equals(routerosType)) {
+                return current;
+            }
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosL2TPCliInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosL2TPCliInterface.java
new file mode 100644 (file)
index 0000000..d4e2c4e
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosL2TPCliInterface} is a model class for `l2tp-out` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosL2TPCliInterface extends RouterosInterfaceBase {
+    public RouterosL2TPCliInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.L2TP_SERVER;
+    }
+
+    @Override
+    public String getApiType() {
+        return "l2tp-client";
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return false;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        return true;
+    }
+
+    public @Nullable String getStatus() {
+        return getProp("status");
+    }
+
+    public @Nullable String getEncoding() {
+        return String.format("Encoding: %s", getProp("encoding", "None"));
+    }
+
+    public @Nullable String getServerAddress() {
+        return getProp("connect-to");
+    }
+
+    public @Nullable String getLocalAddress() {
+        return getProp("local-address");
+    }
+
+    public @Nullable String getRemoteAddress() {
+        return getProp("remote-address");
+    }
+
+    public @Nullable String getUptime() {
+        return getProp("uptime");
+    }
+
+    public @Nullable LocalDateTime getUptimeStart() {
+        return Converter.routerosPeriodBack(getUptime());
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosL2TPSrvInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosL2TPSrvInterface.java
new file mode 100644 (file)
index 0000000..37e9beb
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosL2TPSrvInterface} is a model class for `l2tp-in` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosL2TPSrvInterface extends RouterosInterfaceBase {
+    public RouterosL2TPSrvInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.L2TP_SERVER;
+    }
+
+    @Override
+    public String getApiType() {
+        return "l2tp-server";
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return true;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        return false;
+    }
+
+    public @Nullable String getStatus() {
+        return getProp("status");
+    }
+
+    public @Nullable String getEncoding() {
+        return String.format("Encoding: %s", getProp("encoding", "None"));
+    }
+
+    public @Nullable String getClientAddress() {
+        return getProp("client-address");
+    }
+
+    public @Nullable String getUptime() {
+        return getProp("uptime");
+    }
+
+    public @Nullable LocalDateTime getUptimeStart() {
+        return Converter.routerosPeriodBack(getUptime());
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosPPPoECliInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosPPPoECliInterface.java
new file mode 100644 (file)
index 0000000..34ad4eb
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosPPPoECliInterface} is a model class for `pppoe-out` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosPPPoECliInterface extends RouterosInterfaceBase {
+    public RouterosPPPoECliInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.PPPOE_CLIENT;
+    }
+
+    @Override
+    public String getApiType() {
+        return "pppoe-client";
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return false;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        return true;
+    }
+
+    public @Nullable String getMacAddress() {
+        return getProp("ac-mac");
+    }
+
+    public @Nullable String getStatus() {
+        return getProp("status");
+    }
+
+    public @Nullable String getUptime() {
+        return getProp("uptime");
+    }
+
+    public @Nullable LocalDateTime getUptimeStart() {
+        return Converter.routerosPeriodBack(getUptime());
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosRegistrationBase.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosRegistrationBase.java
new file mode 100644 (file)
index 0000000..a1e978a
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import static org.openhab.binding.mikrotik.internal.model.RouterosDevice.PROP_ID_KEY;
+
+import java.math.BigInteger;
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosRegistrationBase} is a base model class for WiFi client models having casting accessors for
+ * data that is same for all WiFi client types.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosRegistrationBase extends RouterosBaseData {
+
+    public RouterosRegistrationBase(Map<String, String> props) {
+        super(props);
+        this.postProcess();
+    }
+
+    protected void postProcess() {
+        if (hasProp("bytes")) {
+            String bytesStr = getProp("bytes");
+            if (bytesStr != null) {
+                String[] bytes = bytesStr.split(",");
+                setProp("tx-byte", bytes[0]);
+                setProp("rx-byte", bytes[1]);
+            }
+        }
+        if (hasProp("packets")) {
+            String packetsStr = getProp("packets");
+            if (packetsStr != null) {
+                String[] packets = packetsStr.split(",");
+                setProp("tx-packet", packets[0]);
+                setProp("rx-packet", packets[1]);
+            }
+        }
+    }
+
+    public @Nullable String getId() {
+        return getProp(PROP_ID_KEY);
+    }
+
+    public @Nullable String getName() {
+        return getProp("name");
+    }
+
+    public @Nullable String getComment() {
+        return getProp("comment");
+    }
+
+    public @Nullable String getMacAddress() {
+        return getProp("mac-address");
+    }
+
+    public @Nullable String getSSID() {
+        return getProp("ssid");
+    }
+
+    public @Nullable String getInterfaceName() {
+        return getProp("interface");
+    }
+
+    public @Nullable BigInteger getTxBytes() {
+        return getBigIntProp("tx-byte");
+    }
+
+    public @Nullable BigInteger getRxBytes() {
+        return getBigIntProp("rx-byte");
+    }
+
+    public @Nullable BigInteger getTxPackets() {
+        return getBigIntProp("tx-packet");
+    }
+
+    public @Nullable BigInteger getRxPackets() {
+        return getBigIntProp("rx-packet");
+    }
+
+    public @Nullable String getUptime() {
+        return getProp("uptime");
+    }
+
+    public @Nullable LocalDateTime getUptimeStart() {
+        return Converter.routerosPeriodBack(getUptime());
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosRouterboardInfo.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosRouterboardInfo.java
new file mode 100644 (file)
index 0000000..e8bd061
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RouterosRouterboardInfo} is a model class for RouterOS system info used as bridge thing property values.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosRouterboardInfo extends RouterosBaseData {
+
+    public RouterosRouterboardInfo(Map<String, String> props) {
+        super(props);
+    }
+
+    public String getFirmware() {
+        return String.format("v%s (%s)", getFirmwareVersion(), getFirmwareType());
+    }
+
+    public boolean isRouterboard() {
+        return getProp("routerboard", "").equals("true");
+    }
+
+    public String getModel() {
+        return getProp("model", "Unknown");
+    }
+
+    public String getSerialNumber() {
+        return getProp("serial-number", "XXX");
+    }
+
+    public String getFirmwareType() {
+        return getProp("firmware-type", "N/A");
+    }
+
+    public String getFirmwareVersion() {
+        return getProp("current-firmware", "Unknown");
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosSystemResources.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosSystemResources.java
new file mode 100644 (file)
index 0000000..d230b91
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosSystemResources} is a model class for RouterOS system info having casting accessors for
+ * data that is available through bridge thing channels.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosSystemResources extends RouterosBaseData {
+
+    public RouterosSystemResources(Map<String, String> props) {
+        super(props);
+    }
+
+    public @Nullable Integer getFreeSpace() {
+        return getIntProp("free-hdd-space");
+    }
+
+    public @Nullable Integer getTotalSpace() {
+        return getIntProp("total-hdd-space");
+    }
+
+    public @Nullable Integer getSpaceUse() {
+        Integer freeSpace = getFreeSpace(), totalSpace = getTotalSpace();
+        if (freeSpace == null || totalSpace == null) {
+            return null;
+        }
+        return 100 - Math.round(100F * freeSpace / totalSpace);
+    }
+
+    public @Nullable Integer getFreeMem() {
+        return getIntProp("free-memory");
+    }
+
+    public @Nullable Integer getTotalMem() {
+        return getIntProp("total-memory");
+    }
+
+    public @Nullable Integer getMemUse() {
+        Integer freeMem = getFreeMem(), totalMem = getTotalMem();
+        if (freeMem == null || totalMem == null) {
+            return null;
+        }
+        return 100 - Math.round(100F * freeMem / totalMem);
+    }
+
+    public @Nullable Integer getCpuLoad() {
+        return getIntProp("cpu-load");
+    }
+
+    public @Nullable String getUptime() {
+        return getProp("uptime");
+    }
+
+    public @Nullable LocalDateTime getUptimeStart() {
+        return Converter.routerosPeriodBack(getUptime());
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosWirelessRegistration.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosWirelessRegistration.java
new file mode 100644 (file)
index 0000000..dfc5786
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mikrotik.internal.util.Converter;
+
+/**
+ * The {@link RouterosWirelessRegistration} is a model class for WiFi client data retrieced from RouterOS
+ * physical wireless interface. Is a subclass of {@link RouterosRegistrationBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosWirelessRegistration extends RouterosRegistrationBase {
+    public RouterosWirelessRegistration(Map<String, String> props) {
+        super(props);
+    }
+
+    public int getRxSignal() {
+        String signalValue = getProp("signal-strength", "0@hz").split("@")[0];
+        return Integer.parseInt(signalValue);
+    }
+
+    public @Nullable LocalDateTime getLastActivity() {
+        return Converter.routerosPeriodBack(getProp("last-activity"));
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosWlanInterface.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosWlanInterface.java
new file mode 100644 (file)
index 0000000..39ebc28
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link RouterosWlanInterface} is a model class for `waln` interface models having casting accessors for
+ * data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RouterosWlanInterface extends RouterosInterfaceBase {
+    public RouterosWlanInterface(Map<String, String> props) {
+        super(props);
+    }
+
+    @Override
+    public RouterosInterfaceType getDesignedType() {
+        return RouterosInterfaceType.WLAN;
+    }
+
+    @Override
+    public String getApiType() {
+        return "wireless";
+    }
+
+    @Override
+    public boolean hasDetailedReport() {
+        return true;
+    }
+
+    @Override
+    public boolean hasMonitor() {
+        return true;
+    }
+
+    public boolean isMaster() {
+        return !getProp("master-interface", "").isBlank();
+    }
+
+    public @Nullable String getCurrentState() {
+        return getProp("status");
+    }
+
+    public @Nullable String getSSID() {
+        return getProp("ssid");
+    }
+
+    public @Nullable String getMode() {
+        return getProp("mode");
+    }
+
+    public @Nullable String getRate() {
+        return getProp("band");
+    }
+
+    public @Nullable String getInterfaceType() {
+        return getProp("interface-type");
+    }
+
+    public int getRegisteredClients() {
+        Integer registeredClients = getIntProp("registered-clients");
+        return registeredClients == null ? 0 : registeredClients;
+    }
+
+    public int getAuthorizedClients() {
+        Integer authedClients = getIntProp("authenticated-clients");
+        return authedClients == null ? 0 : authedClients;
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/Converter.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/Converter.java
new file mode 100644 (file)
index 0000000..0233120
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.util;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link Converter} is a utility class having functions to convert RouterOS-specific data representation strings
+ * to regular Java types.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class Converter {
+    private static final DateTimeFormatter ROUTEROS_FORMAT = DateTimeFormatter.ofPattern("MMM/dd/yyyy kk:mm:ss",
+            Locale.ENGLISH);
+
+    private static final Pattern PERIOD_PATTERN = Pattern.compile("(\\d+)([a-z]+){1,3}");
+
+    public @Nullable static LocalDateTime fromRouterosTime(@Nullable String dateTimeString) {
+        if (dateTimeString == null) {
+            return null;
+        }
+        String fixedTs = dateTimeString.substring(0, 1).toUpperCase() + dateTimeString.substring(1);
+        return LocalDateTime.parse(fixedTs, ROUTEROS_FORMAT);
+    }
+
+    public @Nullable static LocalDateTime routerosPeriodBack(@Nullable String durationString) {
+        return routerosPeriodBack(durationString, LocalDateTime.now());
+    }
+
+    public @Nullable static LocalDateTime routerosPeriodBack(@Nullable String durationString,
+            LocalDateTime fromDateTime) {
+        if (durationString == null) {
+            return null;
+        }
+
+        Matcher m = PERIOD_PATTERN.matcher(durationString);
+        LocalDateTime ts = fromDateTime;
+        while (m.find()) {
+            int amount = Integer.parseInt(m.group(1));
+            String periodKey = m.group(2);
+            switch (periodKey) {
+                case "y":
+                    ts = ts.minusYears(amount);
+                    break;
+                case "w":
+                    ts = ts.minusWeeks(amount);
+                    break;
+                case "d":
+                    ts = ts.minusDays(amount);
+                    break;
+                case "h":
+                    ts = ts.minusHours(amount);
+                    break;
+                case "m":
+                    ts = ts.minusMinutes(amount);
+                    break;
+                case "s":
+                    ts = ts.minusSeconds(amount);
+                    break;
+                case "ms":
+                    ts = ts.plus(amount, ChronoField.MILLI_OF_SECOND.getBaseUnit());
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            String.format("Unable to parse duration %s - %s is unknown", durationString, periodKey));
+            }
+        }
+        return ts;
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/RateCalculator.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/RateCalculator.java
new file mode 100644 (file)
index 0000000..43d8692
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.util;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.threeten.extra.Seconds;
+
+/**
+ * The {@link RateCalculator} is used to calculate data changing rate as number per second. Has a separate method
+ * to get megabits per second rate out of byte number.
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class RateCalculator {
+    public static final int BYTES_IN_MEGABIT = 125000;
+
+    private BigDecimal value;
+    float rate;
+    LocalDateTime lastUpdated;
+
+    public RateCalculator(BigDecimal initialValue) {
+        this.value = initialValue;
+        this.lastUpdated = LocalDateTime.now();
+        this.rate = 0.0F;
+    }
+
+    public float getRate() {
+        return this.rate;
+    }
+
+    public float getMegabitRate() {
+        return getRate() / BYTES_IN_MEGABIT;
+    }
+
+    public void update(@Nullable BigDecimal currentValue) {
+        if (currentValue != null) {
+            synchronized (this) {
+                LocalDateTime thisUpdated = LocalDateTime.now();
+                Seconds secDiff = Seconds.between(lastUpdated, thisUpdated);
+                this.rate = currentValue.subtract(value).floatValue() / secDiff.getAmount();
+                this.value = currentValue;
+                this.lastUpdated = thisUpdated;
+            }
+        }
+    }
+
+    public void update(@Nullable BigInteger currentValue) {
+        BigInteger val = currentValue == null ? BigInteger.ZERO : currentValue;
+        this.update(new BigDecimal(val));
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/StateUtil.java b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/util/StateUtil.java
new file mode 100644 (file)
index 0000000..5f63d1d
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.util;
+
+import java.math.BigInteger;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link StateUtil} class holds static methods to cast Java native/class types to OpenHAB values
+ *
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class StateUtil {
+
+    public static State stringOrNull(@Nullable String value) {
+        return value == null ? UnDefType.NULL : new StringType(value);
+    }
+
+    public static State qtyMegabitPerSecOrNull(@Nullable Float value) {
+        return value == null ? UnDefType.NULL : new QuantityType<>(value, Units.MEGABIT_PER_SECOND);
+    }
+
+    public static State qtyPercentOrNull(@Nullable Integer value) {
+        return value == null ? UnDefType.NULL : new QuantityType<>(value, Units.PERCENT);
+    }
+
+    public static State qtyBytesOrNull(@Nullable Integer value) {
+        return value == null ? UnDefType.NULL : new QuantityType<>(value, Units.BYTE);
+    }
+
+    public static State intOrNull(@Nullable Integer value) {
+        return value == null ? UnDefType.NULL : new DecimalType(value.floatValue());
+    }
+
+    public static State bigIntOrNull(@Nullable BigInteger value) {
+        return value == null ? UnDefType.NULL : DecimalType.valueOf(value.toString());
+    }
+
+    public static State floatOrNull(@Nullable Float value) {
+        return value == null ? UnDefType.NULL : new DecimalType(value);
+    }
+
+    public static State boolOrNull(@Nullable Boolean value) {
+        if (value == null) {
+            return UnDefType.NULL;
+        }
+        return value ? OnOffType.ON : OnOffType.OFF;
+    }
+
+    public static State timeOrNull(@Nullable LocalDateTime value) {
+        return value == null ? UnDefType.NULL : new DateTimeType(value.atZone(ZoneId.systemDefault()));
+    }
+}
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mikrotik/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..86332f0
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="mikrotik" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Mikrotik Binding</name>
+       <description>
+               This is the binding for integrating Mikrotik RouterOS powered devices (routers, access points, switches,
+               etc) to facilitate WiFi clients and network interface tracking.
+       </description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.mikrotik/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mikrotik/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..7297b93
--- /dev/null
@@ -0,0 +1,414 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mikrotik"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="routeros">
+               <label>Mikrotik RouterOS</label>
+               <description>A connection to RouterOS device</description>
+               <channels>
+                       <channel id="freeSpace" typeId="freeSpace"/>
+                       <channel id="totalSpace" typeId="totalSpace"/>
+                       <channel id="usedSpace" typeId="usedSpace"/>
+                       <channel id="freeMemory" typeId="freeMemory"/>
+                       <channel id="totalMemory" typeId="totalMemory"/>
+                       <channel id="usedMemory" typeId="usedMemory"/>
+                       <channel id="cpuLoad" typeId="cpuLoad"/>
+                       <channel id="upSince" typeId="upSince"/>
+               </channels>
+               <properties>
+                       <property name="vendor">Mikrotik</property>
+                       <property name="modelId">RouterOS</property>
+                       <property name="firmware"/>
+                       <property name="serial"/>
+               </properties>
+               <representation-property>name</representation-property>
+               <config-description>
+                       <parameter name="host" type="text" required="true">
+                               <label>Hostname</label>
+                               <description>Hostname or IP address of the RouterOS device</description>
+                               <default>192.168.88.1</default>
+                               <context>network-address</context>
+                       </parameter>
+                       <parameter name="port" type="integer" max="65535" min="1" required="false">
+                               <label>API Port</label>
+                               <description>API Port number of the RouterOS device</description>
+                               <default>8728</default>
+                       </parameter>
+                       <parameter name="login" type="text" required="true">
+                               <label>Username</label>
+                               <default>admin</default>
+                               <description>The username to access the the RouterOS device</description>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <label>Password</label>
+                               <description>The user password to access the RouterOS device</description>
+                               <context>password</context>
+                       </parameter>
+                       <parameter name="refresh" type="integer" required="false">
+                               <label>Refresh Interval</label>
+                               <description>The refresh interval in seconds to poll the RouterOS device</description>
+                               <default>10</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="interface">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="routeros"/>
+               </supported-bridge-type-refs>
+               <label>RouterOS Interface</label>
+               <description>A network interface from RouterOS system (ethernet, wifi, vpn, etc.)</description>
+               <channels>
+                       <!-- common channels for all interface types -->
+                       <channel id="type" typeId="interfaceType"/>
+                       <channel id="name" typeId="interfaceName"/>
+                       <channel id="comment" typeId="comment"/>
+                       <channel id="macAddress" typeId="macAddress"/>
+                       <channel id="enabled" typeId="enabled"/>
+                       <channel id="connected" typeId="connected"/>
+                       <channel id="lastLinkDownTime" typeId="lastLinkDownTime"/>
+                       <channel id="lastLinkUpTime" typeId="lastLinkUpTime"/>
+                       <channel id="linkDowns" typeId="linkDowns"/>
+                       <channel id="txRate" typeId="txRate"/>
+                       <channel id="rxRate" typeId="rxRate"/>
+                       <channel id="txPacketRate" typeId="txPacketRate"/>
+                       <channel id="rxPacketRate" typeId="rxPacketRate"/>
+                       <channel id="txBytes" typeId="txBytes"/>
+                       <channel id="rxBytes" typeId="rxBytes"/>
+                       <channel id="txPackets" typeId="txPackets"/>
+                       <channel id="rxPackets" typeId="rxPackets"/>
+                       <channel id="txDrops" typeId="txDrops"/>
+                       <channel id="rxDrops" typeId="rxDrops"/>
+                       <channel id="txErrors" typeId="txErrors"/>
+                       <channel id="rxErrors" typeId="rxErrors"/>
+                       <!-- ethernet interface channels -->
+                       <channel id="defaultName" typeId="defaultName"/>
+                       <channel id="rate" typeId="ethernetRate"/>
+                       <!-- cap interface channels -->
+                       <channel id="state" typeId="state"/>
+                       <channel id="registeredClients" typeId="registeredClients"/>
+                       <channel id="authorizedClients" typeId="authorizedClients"/>
+                       <!-- ppp & vpn interface channels -->
+                       <channel id="upSince" typeId="upSince"/>
+               </channels>
+               <representation-property>name</representation-property>
+               <config-description>
+                       <parameter name="name" type="text" required="true">
+                               <label>Interface Name</label>
+                               <description>RouterOS Interface name (i.e. ether1)</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="wifiRegistration">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="routeros"/>
+               </supported-bridge-type-refs>
+               <label>RouterOS Wireless Client</label>
+               <description>A wireless client connected to a RouterOS wireless network (direct or CAPsMAN-managed)</description>
+               <channels>
+                       <channel id="macAddress" typeId="macAddress"/>
+                       <channel id="comment" typeId="comment"/>
+                       <channel id="connected" typeId="connected"/>
+                       <channel id="continuous" typeId="continuous"/>
+                       <channel id="ssid" typeId="ssid"/>
+                       <channel id="interface" typeId="interfaceName"/>
+                       <channel id="signal" typeId="system.signal-strength"/>
+                       <channel id="upSince" typeId="upSince"/>
+                       <channel id="lastSeen" typeId="lastSeen"/>
+                       <channel id="txRate" typeId="txRate"/>
+                       <channel id="rxRate" typeId="rxRate"/>
+                       <channel id="txPacketRate" typeId="txPacketRate"/>
+                       <channel id="rxPacketRate" typeId="rxPacketRate"/>
+                       <channel id="txBytes" typeId="txBytes"/>
+                       <channel id="rxBytes" typeId="rxBytes"/>
+                       <channel id="txPackets" typeId="txPackets"/>
+                       <channel id="rxPackets" typeId="rxPackets"/>
+               </channels>
+               <representation-property>macAddress</representation-property>
+               <config-description>
+                       <parameter name="mac" type="text" required="true">
+                               <label>Client MAC</label>
+                               <description>WiFi client MAC address</description>
+                       </parameter>
+                       <parameter name="ssid" type="text" required="false">
+                               <label>SSID</label>
+                               <description>
+                                       Constraining SSID for the WiFi client (optional). If client will connect to another SSID,
+                                       this thing
+                                       will stay offline until client reconnects to specified SSID.
+                               </description>
+                       </parameter>
+                       <parameter name="considerContinuous" type="integer" required="false">
+                               <label>Consider Home Interval</label>
+                               <description>The interval in seconds to treat the client as connected permanently</description>
+                               <default>180</default>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+
+       <channel-type id="freeSpace" advanced="true">
+               <item-type>Number:DataAmount</item-type>
+               <label>Free Space</label>
+               <description>Amount of free storage left on device in bytes</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="totalSpace" advanced="true">
+               <item-type>Number:DataAmount</item-type>
+               <label>Total Space</label>
+               <description>Amount of total storage available on device in bytes</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="usedSpace">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Used Space %</label>
+               <description>Percentage of used device storage space</description>
+               <state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="freeMemory" advanced="true">
+               <item-type>Number:DataAmount</item-type>
+               <label>Free RAM</label>
+               <description>Amount of free memory left on device in bytes</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="totalMemory" advanced="true">
+               <item-type>Number:DataAmount</item-type>
+               <label>Total RAM</label>
+               <description>Amount of total memory available on device in bytes</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="usedMemory">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Used RAM %</label>
+               <description>Percentage of used device memory</description>
+               <state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="cpuLoad">
+               <item-type>Number:Dimensionless</item-type>
+               <label>CPU Load %</label>
+               <description>CPU load percentage</description>
+               <state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
+       </channel-type>
+
+
+
+       <channel-type id="interfaceType" advanced="true">
+               <item-type>String</item-type>
+               <label>Interface Type</label>
+               <description>Network interface type</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="interfaceName" advanced="true">
+               <item-type>String</item-type>
+               <label>Interface Name</label>
+               <description>Network interface name</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="comment" advanced="true">
+               <item-type>String</item-type>
+               <label>Comment</label>
+               <description>User-defined comment</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="macAddress" advanced="true">
+               <item-type>String</item-type>
+               <label>MAC Address</label>
+               <description>MAC address of the client or interface</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="enabled">
+               <item-type>Switch</item-type>
+               <label>Enabled</label>
+               <description>Reflects enabled or disabled state</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="connected">
+               <item-type>Switch</item-type>
+               <label>Connected</label>
+               <description>Reflects connected or disconnected state</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="continuous">
+               <item-type>Switch</item-type>
+               <label>Continuous</label>
+               <description>Connection is considered long-running</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="lastLinkDownTime">
+               <item-type>DateTime</item-type>
+               <label>Last Link Down</label>
+               <description>Last time when link went down</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="lastLinkUpTime">
+               <item-type>DateTime</item-type>
+               <label>Last Link Up</label>
+               <description>Last time when link went up</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="linkDowns" advanced="true">
+               <item-type>Number</item-type>
+               <label>Link Downs</label>
+               <description>Amount of link downs</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="txRate">
+               <item-type>Number:DataTransferRate</item-type>
+               <label>Transmission Rate</label>
+               <description>Rate of data transmission in megabits per second</description>
+               <state readOnly="true" pattern="%.2f %unit%"/>
+       </channel-type>
+
+       <channel-type id="rxRate">
+               <item-type>Number:DataTransferRate</item-type>
+               <label>Receiving Rate</label>
+               <description>Rate of data receiving in megabits per second</description>
+               <state readOnly="true" pattern="%.2f %unit%"/>
+       </channel-type>
+
+       <channel-type id="txPacketRate" advanced="true">
+               <item-type>Number</item-type>
+               <label>Transmission Packet Rate</label>
+               <description>Rate of data transmission in packets per second</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="rxPacketRate" advanced="true">
+               <item-type>Number</item-type>
+               <label>Receiving Packet Rate</label>
+               <description>Rate of data receiving in packets per second</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="txBytes" advanced="true">
+               <item-type>Number:DataAmount</item-type>
+               <label>Transmitted Bytes</label>
+               <description>Amount of bytes transmitted</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="rxBytes" advanced="true">
+               <item-type>Number:DataAmount</item-type>
+               <label>Received Bytes</label>
+               <description>Amount of bytes received</description>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="txPackets" advanced="true">
+               <item-type>Number</item-type>
+               <label>Transmitted Packets</label>
+               <description>Amount of packets transmitted</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="rxPackets" advanced="true">
+               <item-type>Number</item-type>
+               <label>Received Packets</label>
+               <description>Amount of packets received</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="txDrops" advanced="true">
+               <item-type>Number</item-type>
+               <label>Transmission Drops</label>
+               <description>Amount of packets dropped during transmission</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="rxDrops" advanced="true">
+               <item-type>Number</item-type>
+               <label>Receiving Drops</label>
+               <description>Amount of packets dropped during receiving</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="txErrors" advanced="true">
+               <item-type>Number</item-type>
+               <label>Transmission Errors</label>
+               <description>Amount of errors during transmission</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="rxErrors" advanced="true">
+               <item-type>Number</item-type>
+               <label>Receiving Errors</label>
+               <description>Amount of errors during receiving</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="defaultName" advanced="true">
+               <item-type>String</item-type>
+               <label>Default Name</label>
+               <description>Interface factory name</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="ethernetRate" advanced="true">
+               <item-type>String</item-type>
+               <label>Link Rate</label>
+               <description>Ethernet link rate</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="state">
+               <item-type>String</item-type>
+               <label>State</label>
+               <description>WiFi interface state</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="registeredClients">
+               <item-type>Number</item-type>
+               <label>Registered Clients</label>
+               <description>Amount of clients registered to WiFi interface</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="authorizedClients">
+               <item-type>Number</item-type>
+               <label>Authorized Clients</label>
+               <description>Amount of clients authorized by WiFi interface</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="upSince">
+               <item-type>DateTime</item-type>
+               <label>Up since</label>
+               <description>Time when thing got up</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="lastSeen">
+               <item-type>DateTime</item-type>
+               <label>Last Seen</label>
+               <description>Time of when the client was last seen connected</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="ssid">
+               <item-type>String</item-type>
+               <label>WiFi Network</label>
+               <description>Wireless Network (SSID) the wireless client is connected to</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mikrotik/src/test/java/org/openhab/binding/mikrotik/internal/util/ConverterTest.java b/bundles/org.openhab.binding.mikrotik/src/test/java/org/openhab/binding/mikrotik/internal/util/ConverterTest.java
new file mode 100644 (file)
index 0000000..4eb64b1
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 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.mikrotik.internal.util;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Oleg Vivtash - Initial contribution
+ */
+@NonNullByDefault
+public class ConverterTest {
+
+    @Test
+    public void testFromRouterosTime() {
+        assertThat(Converter.fromRouterosTime("dec/11/2020 20:45:40"),
+                is(equalTo(LocalDateTime.of(2020, 12, 11, 20, 45, 40, 0))));
+        assertThat(Converter.fromRouterosTime("jan/07/2021 09:14:11"),
+                is(equalTo(LocalDateTime.of(2021, 1, 7, 9, 14, 11, 0))));
+        assertThat(Converter.fromRouterosTime("feb/13/2021 23:59:59"),
+                is(equalTo(LocalDateTime.of(2021, 2, 13, 23, 59, 59, 0))));
+    }
+
+    @Test
+    public void testFromRouterosPeriod() {
+        LocalDateTime fromDateTime = LocalDateTime.of(2021, 2, 1, 0, 0, 0, 0);
+
+        assertThat(Converter.routerosPeriodBack("1y3w4d5h6m7s11ms", fromDateTime),
+                is(equalTo(LocalDateTime.parse("2020-01-06T18:53:53.011"))));
+
+        assertNull(Converter.routerosPeriodBack(null));
+
+        /*
+         * uptime = 6w6h31m31s
+         * uptime = 3d7h6m43s710ms
+         * uptime = 16h39m58s220ms
+         * uptime = 1h38m53s110ms
+         * uptime = 53m53s950ms
+         */
+
+        assertThat(Converter.routerosPeriodBack("6w6h31m31s", fromDateTime),
+                is(equalTo(LocalDateTime.parse("2020-12-20T17:28:29"))));
+
+        assertThat(Converter.routerosPeriodBack("3d7h6m43s710ms", fromDateTime),
+                is(equalTo(LocalDateTime.parse("2021-01-28T16:53:17.710"))));
+
+        assertThat(Converter.routerosPeriodBack("16h39m58s220ms", fromDateTime),
+                is(equalTo(LocalDateTime.parse("2021-01-31T07:20:02.220"))));
+
+        assertThat(Converter.routerosPeriodBack("1h38m53s110ms", fromDateTime),
+                is(equalTo(LocalDateTime.parse("2021-01-31T22:21:07.110"))));
+
+        assertThat(Converter.routerosPeriodBack("53m53s950ms", fromDateTime),
+                is(equalTo(LocalDateTime.parse("2021-01-31T23:06:07.950"))));
+    }
+}
index 8df22715fdc54a794b0c4e2b59e692616c471428..73f6869e16a801359776c0e691407618fefcb72c 100644 (file)
     <module>org.openhab.binding.mielecloud</module>
     <module>org.openhab.binding.mihome</module>
     <module>org.openhab.binding.miio</module>
+    <module>org.openhab.binding.mikrotik</module>
     <module>org.openhab.binding.millheat</module>
     <module>org.openhab.binding.milight</module>
     <module>org.openhab.binding.minecraft</module>