diff --git a/CODEOWNERS b/CODEOWNERS
index 5471b3eb9ad..a9bb24764bb 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -172,6 +172,7 @@
/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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 3c984e667d6..c86eafdcd7a 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -848,6 +848,11 @@
org.openhab.binding.miio
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.mikrotik
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.milight
diff --git a/bundles/org.openhab.binding.mikrotik/NOTICE b/bundles/org.openhab.binding.mikrotik/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/NOTICE
@@ -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
index 00000000000..43d74e60634
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/README.md
@@ -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 [IP -> Services](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 System -> Users 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
index 00000000000..08a7da4226b
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.2.0-SNAPSHOT
+
+
+ org.openhab.binding.mikrotik
+
+ openHAB Add-ons :: Bundles :: Mikrotik Binding
+
+
+
+ me.legrange
+ mikrotik
+ 3.0.7
+ compile
+
+
+
+
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
index 00000000000..50de4336830
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.mikrotik/${project.version}
+
+
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
index 00000000000..4463c40c7c3
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikBindingConstants.java
@@ -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
index 00000000000..2a3f9cc4c33
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/MikrotikHandlerFactory.java
@@ -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
index 00000000000..e4133bdcad2
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/ConfigValidation.java
@@ -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
index 00000000000..5a20255cfa9
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/InterfaceThingConfig.java
@@ -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
index 00000000000..54cf09404c4
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/RouterosThingConfig.java
@@ -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
index 00000000000..14cac0effcf
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/config/WirelessClientThingConfig.java
@@ -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
index 00000000000..8e53619a789
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/ChannelUpdateException.java
@@ -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
index 00000000000..87bdbae60a5
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikBaseThingHandler.java
@@ -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 config - the config class used by this base thing handler
+ *
+ */
+@NonNullByDefault
+public abstract class MikrotikBaseThingHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(MikrotikBaseThingHandler.class);
+ protected @Nullable C config;
+ private @Nullable ScheduledFuture> refreshJob;
+ protected ExpiringCache refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false);
+ protected Map 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
index 00000000000..a7f251f502a
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikInterfaceThingHandler.java
@@ -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 {
+ 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
index 00000000000..e31675ec985
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikRouterosBridgeHandler.java
@@ -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 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 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
index 00000000000..20aec9d57f3
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/handler/MikrotikWirelessClientThingHandler.java
@@ -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 {
+ 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
index 00000000000..3dd9a8416da
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBaseData.java
@@ -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 propMap;
+
+ public RouterosBaseData(Map props) {
+ this.propMap = props;
+ }
+
+ public void mergeProps(Map 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
index 00000000000..49ba8001457
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosBridgeInterface.java
@@ -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 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
index 00000000000..0be4e892ffd
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapInterface.java
@@ -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 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
index 00000000000..4d3c200cdf7
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosCapsmanRegistration.java
@@ -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 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
index 00000000000..47361328167
--- /dev/null
+++ b/bundles/org.openhab.binding.mikrotik/src/main/java/org/openhab/binding/mikrotik/internal/model/RouterosDevice.java
@@ -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 interfaceCache = new ArrayList<>();
+ private final List capsmanRegistrationCache = new ArrayList<>();
+ private final List wirelessRegistrationCache = new ArrayList<>();
+ private final Set monitoredInterfaces = new HashSet<>();
+ private final Map wlanSsid = new HashMap<>();
+
+ private @Nullable RouterosSystemResources resourcesCache;
+ private @Nullable RouterosRouterboardInfo rbInfo;
+
+ private static Optional createTypedInterface(Map 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 searchResult = capsmanRegistrationCache.stream()
+ .filter(registration -> macAddress.equalsIgnoreCase(registration.getMacAddress())).findFirst();
+ return searchResult.orElse(null);
+ }
+
+ public @Nullable RouterosWirelessRegistration findWirelessRegistration(String macAddress) {
+ Optional searchResult = wirelessRegistrationCache.stream()
+ .filter(registration -> macAddress.equalsIgnoreCase(registration.getMacAddress())).findFirst();
+ return searchResult.orElse(null);
+ }
+
+ @SuppressWarnings("null")
+ public @Nullable RouterosInterfaceBase findInterface(String name) {
+ Optional 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