Openvehicles Monitoring System (OVMS) connection to OH via MQTT

Hi All,

I have a small electric vehicle with an OVMS module installed, this allows an optional MQTT connectivity which can then be connected to openhab.

I have created a combination of a widget, plus the .items and .things files to implement all this.

The OVMS module itself is used in many vehicles, so, my below concept should be re-usable.

My requirements, I wanted a small dashboard giving information about my vehicle charge status, allowing to modify some basic parameters (charge speed and max %ge).

To implement this, there are a few steps needed.

  1. You need to have a MQTT server running, I use mosquitto in a docker container.

  2. You need to install the MQTT addon to openhab and connect it to the above MQTT server.

  3. You need to configure the protocol “Server V3 (MQTT)” in OVMS webui.

  • This is done under the Config tab, also, set it to autostart.
  • Configure your MQTT server, username and password, the topic prefix should be “ovms/”
  • Once this is complete, you should start to see messages arriving in your MQTT server. I use MQTT explorer or MQTTX to view the MQTT server activity.
  1. On Openhab, copy in the .items and .things files.

I do have a “complete” version of these 2 files with all the available parameters from OVMS, but as they are not displayed in the widget it seems pointless to add them all here.

  • openhab/conf/things/ovms.things
Thing mqtt:topic:MQTTBroker:Twizy "Twizy" (mqtt:broker:MQTTBroker) {
    Channels:
        Type number : V_B_Soc "Battery State of Charge" [ stateTopic="ovms/metric/v/b/soc" ]
        Type string : V_C_Charging "Currently Charging" [ stateTopic="ovms/metric/v/c/charging" ]
        Type number : V_C_Climit "Maximum Charger Output Current" [ stateTopic="ovms/metric/v/c/climit" ]
        Type number : V_C_Current "Momentary Charger Output Current" [ stateTopic="ovms/metric/v/c/current" ]
        Type number : V_C_Duration_Soc "Estimated Time Remaining for Sufficient SOC" [ stateTopic="ovms/metric/v/c/duration/soc" ]
        Type number : V_C_Limit_Soc "Sufficient SOC Limit for Current Charge" [ stateTopic="ovms/metric/v/c/limit/soc" ]
        Type number : V_C_Time "Duration of Running Charge" [ stateTopic="ovms/metric/v/c/time" ]
        Type number : Cmd_Charge_Limit_Soc "Set Charge SOC Limit" [ commandTopic="ovms/client/openhab/command/1", formatBeforePublish="config set xrt suffsoc %s", retained=false ]
        Type string : Cmd_Charge_Limit_Soc_Response "Set Charge SOC Limit - Response" [ stateTopic="ovms/client/openhab/response/1" ]
        Type number : Cmd_Charge_Speed "Set Charge Speed Level" [ commandTopic="ovms/client/openhab/command/1", formatBeforePublish="config set xrt chargelevel %s", retained=false ]
        Type number : Cmd_Charge_Speed_Response "Set Charge Speed Level - Response" [ commandTopic="ovms/client/openhab/response/1" ]
}
  • openhab/conf/items/ovms.items
Group Twizy "Twizy (OVMS)" <car>

Number Twizy_V_B_Soc "Battery State of Charge" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_B_Soc" }
String Twizy_V_C_Charging "Currently Charging" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_C_Charging" }
Number Twizy_V_C_Climit "Maximum Charger Output Current" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_C_Climit" }
Number Twizy_V_C_Current "Momentary Charger Output Current" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_C_Current" }
Number Twizy_V_C_Duration_Soc "Estimated Time Remaining for Sufficient SOC" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_C_Duration_Soc" }
Number Twizy_V_C_Limit_Soc "Sufficient SOC Limit for Current Charge" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_C_Limit_Soc" }
Number Twizy_V_C_Time "Duration of Running Charge" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:V_C_Time" }
Number Twizy_Cmd_Charge_Limit_Soc "Set Charge SOC Limit" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:Cmd_Charge_Limit_Soc" }
String Twizy_Cmd_Charge_Limit_Soc_Response "Set Charge SOC Limit - Response" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:Cmd_Charge_Limit_Soc_Response" }
Number Twizy_Cmd_Charge_Speed "Set Charge Speed" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:Cmd_Charge_Speed" }
String Twizy_Cmd_Charge_Speed_Response "Set Charge Speed - Response" (Twizy) { channel="mqtt:topic:MQTTBroker:Twizy:Cmd_Charge_Speed_Response" }

  1. In openhab, create a new widget under developer tab and paste this in:
version: 1
widgets:
  Twizy_All_Props:
    tags:
      - custom
      - dark
    props:
      parameters:
        - context: item
          default: Twizy_V_B_Soc
          description: Battery state of charge
          label: Battery SOC
          name: socItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_V_C_Charging
          description: Charging state
          label: Charging state
          name: chargingItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_V_C_Current
          description: Actual charging current
          label: Charge current
          name: currentItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_V_C_Climit
          description: Maximum charging current
          label: Charge current limit
          name: currentLimitItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_Cmd_Charge_Speed
          description: Maximum charging index
          label: Charge current index limit
          name: currentLimitIndex
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_V_C_Time
          description: Charging duration so far, in seconds
          label: Charge duration
          name: durationItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_V_C_Duration_Soc
          description: Time remaining to the target SOC, in seconds
          label: Time to SOC target
          name: targetTimeItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_V_C_Limit_Soc
          description: Target battery SOC
          label: Target SOC
          name: targetSocItem
          required: true
          type: TEXT
          groupName: general
        - context: item
          default: Twizy_Cmd_Charge_Limit_Soc
          description: Set Target battery SOC
          label: Set Target SOC
          name: targetSocItemSet
          required: true
          type: TEXT
          groupName: general
        - default: Twizy
          label: Headline
          name: headline
          required: false
          type: TEXT
          groupName: header
        - default: twizy.png
          label: icon
          name: heroIcon
          required: false
          type: TEXT
          groupName: images
        - default: 0
          description: Minimum charge current index
          label: Minimum current
          name: currentMin
          required: false
          type: INTEGER
          groupName: controls
        - default: 6
          description: Maximum charge current index
          label: Maximum current
          name: currentMax
          required: false
          type: INTEGER
          groupName: controls
        - default: 1
          description: Charge current step
          label: Current step
          name: currentStep
          required: false
          type: INTEGER
          groupName: controls
      parameterGroups:
        - name: general
          label: General settings
          description: Items used by the Twizy charging widget
        - name: header
          label: Header
        - name: controls
          label: Charge current control
        - name: images
          label: Images
    component: f7-block
    config:
      class:
        - no-padding
        - no-margin
      style:
        position: relative
        user-select: none
        -moz-user-select: none
        -ms-user-select: none
        -webkit-user-select: none
    slots:
      default:
        - component: oh-icon
          config:
            icon: =props.heroIcon
            style:
              max-width: min(max(70px, 18vw), 110px)
              position: absolute
              right: 18px
              top: 8px
              width: 100%
              z-index: 20
        - component: div
          config:
            style:
              position: absolute
              top: 8px
              left: 18px
              right: calc(18px + min(max(70px, 18vw), 110px) / 2)
              height: 60px
              z-index: 15
              pointer-events: none
          slots:
            default:
              - component: div
                config:
                  innerHTML: <style>@keyframes twzflow{0%{left:0;opacity:0}15%{opacity:1}85%{opacity:1}100%{left:100%;opacity:0}}</style>
              - component: div
                config:
                  style:
                    position: absolute
                    top: 50%
                    left: 0
                    right: 0
                    height: 3px
                    margin-top: -1.5px
                    border-radius: 2px
                    background: '=["yes","1","ON","true","TRUE"].includes(String(items[props.chargingItem].state)) ? "rgba(255,197,28,0.35)" : "rgba(255,255,255,0.12)"'
              - component: div
                config:
                  style:
                    position: absolute
                    top: 50%
                    left: 0
                    transform: translateY(-50%)
                    border-radius: 50%
                    background: rgb(255,197,28)
                    display: '=["yes","1","ON","true","TRUE"].includes(String(items[props.chargingItem].state)) ? "block" : "none"'
                    width: =Math.round(6 + Math.min(Math.max(Number.parseFloat(items[props.currentItem].state) || 0, 0), 35) / 35 * 12) + "px"
                    height: =Math.round(6 + Math.min(Math.max(Number.parseFloat(items[props.currentItem].state) || 0, 0), 35) / 35 * 12) + "px"
                    box-shadow: ="0 0 " + Math.round(6 + Math.min(Math.max(Number.parseFloat(items[props.currentItem].state) || 0, 0), 35) / 35 * 12) + "px rgba(255,197,28,0.8)"
                    animation: ="twzflow " + (6 - Math.min(Math.max(Number.parseFloat(items[props.currentItem].state) || 0, 0), 35) / 35 * 4.5).toFixed(2) + "s linear infinite"
        - component: f7-card
          config:
            style:
              background: rgb(57,60,76)
              border-radius: 20px
              margin: 0
              min-height: 330px
              overflow: hidden
              position: relative
          slots:
            default:
              - component: f7-card-content
                config:
                  style:
                    padding: 18px
                    position: relative
                    z-index: 10
                slots:
                  default:
                    - component: f7-row
                      config:
                        style:
                          align-items: center
                          min-height: 45px
                      slots:
                        default:
                          - component: f7-col
                            config:
                              width: 70%
                            slots:
                              default:
                                - component: Label
                                  config:
                                    style:
                                      color: rgba(255,255,255,0.95)
                                      font-size: 19px
                                      font-weight: 600
                                      letter-spacing: 0.5px
                                      text: =props.headline
                                - component: Label
                                  config:
                                    style:
                                      color: '=items[props.chargingItem].state == "yes" || items[props.chargingItem].state == "1" || items[props.chargingItem].state == "ON" || items[props.chargingItem].state == "true" || items[props.chargingItem].state == "TRUE" ? "rgb(255,197,28)" : "rgba(255,255,255,0.55)"'
                                      font-size: 13px
                                      font-weight: 500
                                      margin-top: 3px
                                    text: '=items[props.chargingItem].state == "yes" || items[props.chargingItem].state == "1" || items[props.chargingItem].state == "ON" || items[props.chargingItem].state == "true" || items[props.chargingItem].state == "TRUE" ? "CHARGING" : "NOT CHARGING"'
                    - component: f7-row
                      config:
                        style:
                          justify-content: center
                          margin-top: 8px
                      slots:
                        default:
                          - component: f7-col
                            config:
                              width: 100%
                            slots:
                              default:
                                - component: oh-gauge
                                  config:
                                    type: semicircle
                                    min: 0
                                    max: 100
                                    value: =Number.parseFloat(items[props.socItem].state)
                                    size: 190
                                    borderWidth: 12
                                    borderColor: rgba(255,255,255,0.12)
                                    borderBgColor: rgba(255,255,255,0.12)
                                    valueText: =Number.parseFloat(items[props.socItem].state).toFixed(0) + "%"
                                    valueTextColor: rgba(255,255,255,0.95)
                                    valueFontSize: 28
                                    style:
                                      margin: 0 auto
                                - component: Label
                                  config:
                                    style:
                                      color: rgba(255,255,255,0.55)
                                      display: block
                                      font-size: 12px
                                      margin-top: -10px
                                      text-align: center
                                    text: = "TARGET " + Number.parseFloat(items[props.targetSocItem].state).toFixed(0) + "%"
                    - component: f7-row
                      config:
                        style:
                          align-items: center
                          margin-top: 5px
                      slots:
                        default:
                          - component: f7-col
                            config:
                              width: 45%
                            slots:
                              default:
                                - component: Label
                                  config:
                                    style:
                                      color: rgba(255,255,255,0.55)
                                      font-size: 12px
                                    text: CHARGE CURRENT
                                - component: Label
                                  config:
                                    style:
                                      color: rgba(255,255,255,0.95)
                                      font-size: 22px
                                      font-weight: 600
                                      margin-top: 2px
                                    text: =Number.parseFloat(items[props.currentItem].state).toFixed(1) + " A (" + Number.parseFloat(items[props.currentItem].state*80).toFixed(0) + " w)"
                          - component: f7-col
                            config:
                              width: 55%
                              style:
                                text-align: right
                            slots:
                              default:
                                - component: Label
                                  config:
                                    style:
                                      color: rgba(255,255,255,0.55)
                                      font-size: 12px
                                    text: Charge Speed
                                - component: Label
                                  config:
                                    style:
                                      color: rgb(255,197,28)
                                      font-size: 22px
                                      font-weight: 600
                                      margin-top: 2px
                                    text: =Number.parseFloat(items[props.currentLimitItem].state).toFixed(1) + " A (" + Number.parseFloat(items[props.currentLimitItem].state*80).toFixed(0) + " w)"
                    - component: f7-block
                      config:
                        class:
                          - no-padding
                          - no-margin
                        style:
                          background: rgba(255,255,255,0.10)
                          border-radius: 6px
                          height: 7px
                          margin-top: 10px
                          overflow: hidden
                          width: 100%
                      slots:
                        default:
                          - component: f7-block
                            config:
                              class:
                                - no-padding
                                - no-margin
                              style:
                                background: rgb(255,197,28)
                                border-radius: 6px
                                height: 100%
                                transition: width 0.3s
                                width: =Math.min(100, Math.max(0, Number.parseFloat(items[props.currentItem].state) / Number.parseFloat(items[props.currentLimitItem].state) * 100)) + "%"
                    - component: f7-row
                      config:
                        style:
                          align-items: center
                          margin-top: 14px
                      slots:
                        default:
                          - component: f7-col
                            config:
                              width: 30%
                            slots:
                              default:
                                - component: Label
                                  config:
                                    style:
                                      color: rgba(255,255,255,0.55)
                                      font-size: 11px
                                    text: SET CHARGE SPEED
                          - component: f7-col
                            config:
                              width: 70%
                            slots:
                              default:
                                - component: oh-stepper
                                  config:
                                    item: =props.currentLimitIndex
                                    min: =Number(props.currentMin)
                                    max: =Number(props.currentMax)
                                    step: =Number(props.currentStep)
                                    fill: true
                                    round: true
                                    raised: true
                                    color: yellow
                                    style:
                                      margin: 0
                    - component: f7-row
                      config:
                        style:
                          margin-top: 14px
                      slots:
                        default:
                          - component: f7-col
                            config:
                              width: 50%
                              style:
                                padding-right: 5px
                            slots:
                              default:
                                - component: f7-block
                                  config:
                                    class:
                                      - no-padding
                                    style:
                                      background: rgba(255,255,255,0.07)
                                      border-radius: 12px
                                      margin: 0
                                      padding: 10px
                                  slots:
                                    default:
                                      - component: Label
                                        config:
                                          style:
                                            color: rgba(255,255,255,0.50)
                                            font-size: 11px
                                          text: CHARGING TIME
                                      - component: Label
                                        config:
                                          style:
                                            color: rgba(255,255,255,0.95)
                                            font-size: 17px
                                            font-weight: 600
                                            margin-top: 3px
                                          text: =Math.floor(Number.parseFloat(items[props.durationItem].state) / 3600).toString().padStart(2,"0") + ":" + Math.floor((Number.parseFloat(items[props.durationItem].state) % 3600) / 60).toString().padStart(2,"0")
                    - component: oh-grid-row
                      config: {}
                      slots:
                        default:
                          - component: oh-grid-col
                            config:
                              style:
                                font-size: 17px
                                font-weight: 600
                                margin-top: 3px
                            slots:
                              default:
                                - component: oh-list-card
                                  config: {}
                                  slots:
                                    default:
                                      - component: oh-input-item
                                        config:
                                          icon: oh:battery
                                          inputmode: numeric
                                          item: =props.targetSocItemSet
                                          outline: true
                                          sendButton: true
                                          showTime: false
                                          min: 60
                                          max: 100
                                          step: 1
                                          title: Max Charge
                                          type: number
                                          useDisplayState: true
                                          style:
                                            background: rgb(57,60,76)
                    - component: f7-col
                      config:
                        width: 50%
                        style:
                          text-align: right
                      slots:
                        default:
                          - component: Label
                            config:
                              style:
                                color: rgba(255,255,255,0.45)
                                font-size: 11px
                              text: STATUS
                          - component: Label
                            config:
                              style:
                                color: '=items[props.chargingItem].state == "yes" || items[props.chargingItem].state == "1" || items[props.chargingItem].state == "ON" || items[props.chargingItem].state == "true" || items[props.chargingItem].state == "TRUE" ? "rgb(255,197,28)" : "rgba(255,255,255,0.65)"'
                                font-size: 14px
                                font-weight: 600
                              text: '=items[props.chargingItem].state == "yes" || items[props.chargingItem].state == "1" || items[props.chargingItem].state == "ON" || items[props.chargingItem].state == "true" || items[props.chargingItem].state == "TRUE" ? "CHARGING" : "IDLE"'

This widget can then be used in any of your layouts / overview page.

Finally, add the below icon as openhab/conf/icons/classic/twizy.png

Hope someone else finds this useful!

If you post this widget to the marketplace, users can install it like a binding instead of copy and paste.

Hi Rich,

I would love to do this, but I am not sure how!..

I have never done this before, are there any guides around that show the required steps?

I know that all the files need to be hosted elsewhere, e.g. github, but to go from placing a few files in github, to a fully working addon is a bit daunting.

Go to the Marketplace → UI Widgets category of the forum, create a new post and follow the instructions in the template that populates the post.