Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
6
.babelrc
6
.babelrc
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["@babel/env"],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-syntax-class-properties"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
env:
|
|
||||||
node: true
|
|
||||||
browser: false
|
|
||||||
commonjs: true
|
|
||||||
es6: true
|
|
||||||
mocha: true
|
|
||||||
extends:
|
|
||||||
- eslint:recommended
|
|
||||||
parser: "@babel/eslint-parser"
|
|
||||||
parserOptions:
|
|
||||||
babelOptions:
|
|
||||||
configFile: './.babelrc'
|
|
||||||
ecmaVersion: 2018
|
|
||||||
plugins:
|
|
||||||
- "@babel"
|
|
||||||
rules: {}
|
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -4,8 +4,8 @@ updates:
|
|||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "daily"
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "daily"
|
||||||
|
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@ -16,46 +16,42 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [12.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v4
|
uses: crazy-max/ghaction-docker-meta@v1
|
||||||
with:
|
with:
|
||||||
images: michaelwoods/onstar2mqtt
|
images: michaelwoods/onstar2mqtt
|
||||||
flavor: |
|
tag-sha: true
|
||||||
latest=true
|
tag-schedule: weekly
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=schedule,pattern=weekly
|
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build --if-present
|
- run: npm run build --if-present
|
||||||
- run: npm run lint
|
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Push to DockerHub
|
- name: Push to DockerHub
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@ -1,71 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ main ]
|
|
||||||
schedule:
|
|
||||||
- cron: '28 1 * * 0'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
||||||
# Learn more:
|
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v2
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v2
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v2
|
|
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@ -9,32 +9,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v4
|
uses: crazy-max/ghaction-docker-meta@v1
|
||||||
with:
|
with:
|
||||||
images: michaelwoods/onstar2mqtt
|
images: michaelwoods/onstar2mqtt
|
||||||
flavor: |
|
tag-sha: true
|
||||||
latest=true
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Push to DockerHub
|
- name: Push to DockerHub
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,4 +2,3 @@
|
|||||||
.nyc_output/
|
.nyc_output/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
onstar2mqtt.env
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
FROM node:18-alpine
|
FROM node:12
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ["package.json", "/app/"]
|
COPY ["package.json", "/app/"]
|
||||||
COPY ["package-lock.json", "/app/"]
|
RUN npm install
|
||||||
RUN npm ci --omit=dev --no-fund
|
|
||||||
|
|
||||||
COPY ["src", "/app/src"]
|
COPY ["src", "/app/src"]
|
||||||
|
|
||||||
|
192
HA-MQTT.md
192
HA-MQTT.md
@ -1,100 +1,11 @@
|
|||||||
Sample configs for MQTT Home Assistant integration.
|
Sample configs for MQTT Home Assistant integration.
|
||||||
|
|
||||||
### Commands
|
|
||||||
|
|
||||||
#### example script yaml:
|
|
||||||
```yaml
|
|
||||||
alias: Car - Start Vehicle
|
|
||||||
sequence:
|
|
||||||
- service: mqtt.publish
|
|
||||||
data:
|
|
||||||
topic: homeassistant/YOUR_CAR_VIN/command
|
|
||||||
payload: '{"command": "startVehicle"}'
|
|
||||||
mode: single
|
|
||||||
icon: 'mdi:car-electric'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Triger precondition via calendar
|
|
||||||
````yaml
|
|
||||||
alias: Car Precondition
|
|
||||||
description: Precondition if group.family is home (ie, at least one person).
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id: calendar.YOUR_CAL_NAME
|
|
||||||
from: 'off'
|
|
||||||
to: 'on'
|
|
||||||
condition:
|
|
||||||
- condition: state
|
|
||||||
entity_id: group.family
|
|
||||||
state: home
|
|
||||||
- condition: state
|
|
||||||
entity_id: calendar.YOUR_CAL_NAME
|
|
||||||
state: Bolt Start
|
|
||||||
attribute: message
|
|
||||||
action:
|
|
||||||
- service: script.car_start_vehicle
|
|
||||||
data: {}
|
|
||||||
mode: single
|
|
||||||
````
|
|
||||||
|
|
||||||
### Location
|
|
||||||
Unfortunately, the MQTT Device tracker uses a home/not_home state and the MQTT Json device tracker does not support
|
|
||||||
the discovery schema so a manual entity configuration is required.
|
|
||||||
|
|
||||||
device tracker yaml:
|
|
||||||
```yaml
|
|
||||||
device_tracker:
|
|
||||||
- platform: mqtt_json
|
|
||||||
devices:
|
|
||||||
your_car_name: homeassistant/device_tracker/YOUR_CAR_VIN/getlocation/state
|
|
||||||
```
|
|
||||||
|
|
||||||
#### script yaml:
|
|
||||||
```yaml
|
|
||||||
alias: Car - Location
|
|
||||||
sequence:
|
|
||||||
- service: mqtt.publish
|
|
||||||
data:
|
|
||||||
topic: homeassistant/YOUR_CAR_VIN/command
|
|
||||||
payload: '{"command": "getLocation"}'
|
|
||||||
mode: single
|
|
||||||
icon: 'mdi:map-marker'
|
|
||||||
```
|
|
||||||
### Automation:
|
|
||||||
Create an automation to update the location whenever the odometer changes, instead of on a time interval.
|
|
||||||
```alias: Update EV Location
|
|
||||||
description: ""
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id:
|
|
||||||
- sensor.odometer_mi
|
|
||||||
condition: []
|
|
||||||
action:
|
|
||||||
- service: script.locate_bolt_ev
|
|
||||||
data: {}
|
|
||||||
mode: single
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Commands:
|
|
||||||
[OnStarJS Command Docs](https://github.com/samrum/OnStarJS#commands)
|
|
||||||
1. `getAccountVehicles`
|
|
||||||
2. `startVehicle`
|
|
||||||
3. `cancelStartVehicle`
|
|
||||||
4. `alert`
|
|
||||||
5. `cancelAlert`
|
|
||||||
6. `lockDoor`
|
|
||||||
7. `unlockDoor`
|
|
||||||
8. `chargeOverride`
|
|
||||||
9. `cancelChargeOverride`
|
|
||||||
10. `getLocation`
|
|
||||||
|
|
||||||
|
|
||||||
### Lovelace Dashboard
|
### Lovelace Dashboard
|
||||||
Create a new dashboard, or use the cards in your own view. The `mdi:car-electric` icon works well here.
|
Create a new dashboard, or use the cards in your own view. The `mdi:car-electric` icon works well here.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### dashboard yaml:
|
yaml:
|
||||||
```yaml
|
```yaml
|
||||||
views:
|
views:
|
||||||
- badges: []
|
- badges: []
|
||||||
@ -133,54 +44,22 @@ views:
|
|||||||
icon: 'mdi:car-tire-alert'
|
icon: 'mdi:car-tire-alert'
|
||||||
columns: 2
|
columns: 2
|
||||||
title: Tires
|
title: Tires
|
||||||
|
- type: glance
|
||||||
|
entities:
|
||||||
|
- entity: sensor.last_trip_total_distance
|
||||||
|
name: Distance
|
||||||
|
- entity: sensor.last_trip_electric_econ
|
||||||
|
name: Economy
|
||||||
|
title: Last Trip
|
||||||
- type: entities
|
- type: entities
|
||||||
title: Mileage
|
title: Mileage
|
||||||
entities:
|
entities:
|
||||||
|
- entity: sensor.odometer
|
||||||
|
- entity: sensor.lifetime_energy_used
|
||||||
- entity: sensor.lifetime_mpge
|
- entity: sensor.lifetime_mpge
|
||||||
- entity: sensor.lifetime_efficiency
|
- entity: sensor.lifetime_efficiency
|
||||||
- entity: sensor.electric_economy
|
- entity: sensor.electric_economy
|
||||||
state_color: true
|
- type: glance
|
||||||
footer:
|
|
||||||
type: 'custom:mini-graph-card'
|
|
||||||
entities:
|
|
||||||
- entity: sensor.odometer
|
|
||||||
- entity: sensor.lifetime_energy_used
|
|
||||||
y_axis: secondary
|
|
||||||
show_state: true
|
|
||||||
hours_to_show: 672
|
|
||||||
group_by: date
|
|
||||||
decimals: 0
|
|
||||||
show:
|
|
||||||
graph: bar
|
|
||||||
name: false
|
|
||||||
icon: false
|
|
||||||
- type: entities
|
|
||||||
entities:
|
|
||||||
- entity: binary_sensor.ev_plug_state
|
|
||||||
secondary_info: last-changed
|
|
||||||
- entity: binary_sensor.ev_charge_state
|
|
||||||
secondary_info: last-changed
|
|
||||||
- entity: binary_sensor.priority_charge_indicator
|
|
||||||
- entity: binary_sensor.priority_charge_status
|
|
||||||
- entity: sensor.ev_plug_voltage
|
|
||||||
- entity: sensor.interm_volt_batt_volt
|
|
||||||
- entity: sensor.charger_power_level
|
|
||||||
title: Charging
|
|
||||||
state_color: true
|
|
||||||
- type: 'custom:mini-graph-card'
|
|
||||||
entities:
|
|
||||||
- entity: sensor.last_trip_total_distance
|
|
||||||
- entity: sensor.last_trip_electric_econ
|
|
||||||
y_axis: secondary
|
|
||||||
show_state: true
|
|
||||||
name: Last Trip
|
|
||||||
hours_to_show: 672
|
|
||||||
group_by: date
|
|
||||||
agreggate_func: null
|
|
||||||
show:
|
|
||||||
graph: bar
|
|
||||||
icon: false
|
|
||||||
- type: 'custom:mini-graph-card'
|
|
||||||
entities:
|
entities:
|
||||||
- entity: sensor.ambient_air_temperature
|
- entity: sensor.ambient_air_temperature
|
||||||
name: Ambient
|
name: Ambient
|
||||||
@ -188,39 +67,20 @@ views:
|
|||||||
name: Battery
|
name: Battery
|
||||||
- entity: sensor.kewr_daynight_temperature
|
- entity: sensor.kewr_daynight_temperature
|
||||||
name: Outdoor
|
name: Outdoor
|
||||||
name: Temperature
|
title: Temperature
|
||||||
hours_to_show: 24
|
- type: entities
|
||||||
points_per_hour: 1
|
entities:
|
||||||
line_width: 2
|
- entity: binary_sensor.ev_plug_state
|
||||||
- type: grid
|
- entity: binary_sensor.ev_charge_state
|
||||||
cards:
|
- entity: binary_sensor.priority_charge_indicator
|
||||||
- type: button
|
- entity: binary_sensor.priority_charge_status
|
||||||
tap_action:
|
- entity: sensor.ev_plug_voltage
|
||||||
action: toggle
|
- entity: sensor.interm_volt_batt_volt
|
||||||
entity: script.car_start_vehicle
|
- entity: sensor.charger_power_level
|
||||||
name: Start
|
title: Charging
|
||||||
show_state: false
|
|
||||||
- type: button
|
|
||||||
tap_action:
|
|
||||||
action: toggle
|
|
||||||
entity: script.car_cancel_start_vehicle
|
|
||||||
name: Cancel Start
|
|
||||||
show_state: false
|
|
||||||
icon: 'mdi:car-off'
|
|
||||||
- type: button
|
|
||||||
tap_action:
|
|
||||||
action: toggle
|
|
||||||
entity: script.car_lock_doors
|
|
||||||
name: Lock
|
|
||||||
show_state: false
|
|
||||||
icon: 'mdi:car-door-lock'
|
|
||||||
- type: button
|
|
||||||
tap_action:
|
|
||||||
action: toggle
|
|
||||||
entity: script.car_unlock_doors
|
|
||||||
name: Unlock
|
|
||||||
show_state: false
|
|
||||||
icon: 'mdi:car-door'
|
|
||||||
columns: 2
|
|
||||||
title: Bolt EV
|
title: Bolt EV
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
TODO
|
||||||
|
- Utility meter that resets for monthly LIFETIME ENERGY USED. This seems to only be updated after a full charge, along with other data points.
|
29
README.md
29
README.md
@ -1,9 +1,7 @@
|
|||||||
# onstar2mqtt
|
# onstar2mqtt
|
||||||
A service that utilizes the [OnStarJS](https://github.com/samrum/OnStarJS) library to expose OnStar data to MQTT topics.
|
A service that utilizes the [OnStarJS](https://github.com/samrum/OnStarJS) library to expose OnStar data to MQTT topics. Mostly focused around EVs, however happy to accept PRs for other vehicle types.
|
||||||
|
|
||||||
The functionality is mostly focused around EVs (specifically the Bolt EV), however PRs for other vehicle types are certainly welcome.
|
There is no official relationship with GM, Chevrolet nor OnStar. In fact, it would be nice if they'd even respond to development requests, so we wouldn't have to reverse engineer their API.
|
||||||
|
|
||||||
There is no affiliation with this project and GM, Chevrolet nor OnStar. In fact, it would be nice if they'd even respond to development requests so we wouldn't have to reverse engineer their API.
|
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
Collect the following information:
|
Collect the following information:
|
||||||
@ -13,8 +11,8 @@ Collect the following information:
|
|||||||
1. MQTT server information: hostname, username, password
|
1. MQTT server information: hostname, username, password
|
||||||
1. If using TLS, define `MQTT_PORT` and `MQTT_TLS=true`
|
1. If using TLS, define `MQTT_PORT` and `MQTT_TLS=true`
|
||||||
|
|
||||||
Supply these values to the ENV vars below. The default data refresh interval is 30 minutes and can be overridden with ONSTAR_REFRESH with values in milliseconds.
|
Supply these values to the ENV vars below.
|
||||||
### [Docker](https://hub.docker.com/r/michaelwoods/onstar2mqtt)
|
### Docker
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run \
|
docker run \
|
||||||
@ -51,19 +49,12 @@ MQTT_PASSWORD=
|
|||||||
```
|
```
|
||||||
### Node.js
|
### Node.js
|
||||||
It's a typical node.js application, define the same environment values as described in the docker sections and run with:
|
It's a typical node.js application, define the same environment values as described in the docker sections and run with:
|
||||||
`npm run start`. Currently, this is only tested with Node.js 18.x.
|
`npm run start`. Currently only tested with Node.js 12.x.
|
||||||
|
|
||||||
### Home Assistant configuration templates
|
### Home Assistant configuration templates
|
||||||
MQTT auto discovery is enabled. For further integrations and screenshots see [HA-MQTT.md](HA-MQTT.md).
|
MQTT auto discovery is enabled, for further integrations see [HA-MQTT.md](HA-MQTT.md).
|
||||||
|
|
||||||
## Development
|
## TODO
|
||||||
### Running
|
1. Logging library
|
||||||
`npm run start`
|
1. Figure out metric->imperial unit handling
|
||||||
### Testing
|
1. Enable write actions to lock doors, flash lights, remote start, etc.
|
||||||
`npm run test`
|
|
||||||
### Coverage
|
|
||||||
`npm run coverage`
|
|
||||||
### Releases
|
|
||||||
`npm version [major|minor|patch] -m "Version %s" && git push --follow-tags`
|
|
||||||
|
|
||||||
Publish the release on GitHub to trigger a release build (ie, update 'latest' docker tag).
|
|
Binary file not shown.
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 97 KiB |
10341
package-lock.json
generated
10341
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "onstar2mqtt",
|
"name": "onstar2mqtt",
|
||||||
"version": "1.5.4",
|
"version": "1.0.1",
|
||||||
"description": "OnStarJS wrapper for MQTT",
|
"description": "OnStarJS wrapper for MQTT",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"coverage": "nyc npm test",
|
|
||||||
"lint": "npx eslint src test",
|
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"test": "mocha"
|
"test": "mocha"
|
||||||
},
|
},
|
||||||
@ -17,10 +15,7 @@
|
|||||||
"onstar",
|
"onstar",
|
||||||
"mqtt",
|
"mqtt",
|
||||||
"gm",
|
"gm",
|
||||||
"chevrolet",
|
"chevrolet"
|
||||||
"homeassistant",
|
|
||||||
"home-assistant",
|
|
||||||
"home assistant"
|
|
||||||
],
|
],
|
||||||
"author": "Michael Woods",
|
"author": "Michael Woods",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -29,23 +24,14 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/michaelwoods/onstar2mqtt#readme",
|
"homepage": "https://github.com/michaelwoods/onstar2mqtt#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-mqtt": "^2.6.3",
|
"async-mqtt": "^2.6.1",
|
||||||
"convert-units": "^2.3.4",
|
"convert-units": "^2.3.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.20",
|
||||||
"onstarjs": "^2.3.16",
|
"onstarjs": "^2.0.10",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^8.3.1"
|
||||||
"winston": "^3.8.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"mocha": "^8.2.1",
|
||||||
"@babel/eslint-plugin": "^7.19.1",
|
|
||||||
"@babel/plugin-syntax-class-properties": "^7.12.13",
|
|
||||||
"@babel/preset-env": "^7.20.2",
|
|
||||||
"eslint": "^8.35.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
|
||||||
"mocha": "^10.2.0",
|
|
||||||
"nyc": "^15.1.0"
|
"nyc": "^15.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
src/commands.js
126
src/commands.js
@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
class Commands {
|
|
||||||
static CONSTANTS = {
|
|
||||||
ALERT_ACTION: {
|
|
||||||
FLASH: 'Flash',
|
|
||||||
HONK: 'Honk',
|
|
||||||
},
|
|
||||||
ALERT_OVERRIDE: {
|
|
||||||
DOOR_OPEN: 'DoorOpen',
|
|
||||||
IGNITION_ON: 'IgnitionOn'
|
|
||||||
},
|
|
||||||
CHARGE_OVERRIDE: {
|
|
||||||
CHARGE_NOW: 'CHARGE_NOW',
|
|
||||||
CANCEL_OVERRIDE: 'CANCEL_OVERRIDE'
|
|
||||||
},
|
|
||||||
CHARGING_PROFILE_MODE: {
|
|
||||||
DEFAULT_IMMEDIATE: 'DEFAULT_IMMEDIATE',
|
|
||||||
IMMEDIATE: 'IMMEDIATE',
|
|
||||||
DEPARTURE_BASED: 'DEPARTURE_BASED',
|
|
||||||
RATE_BASED: 'RATE_BASED',
|
|
||||||
PHEV_AFTER_MIDNIGHT: 'PHEV_AFTER_MIDNIGHT'
|
|
||||||
},
|
|
||||||
CHARGING_PROFILE_RATE: {
|
|
||||||
OFFPEAK: 'OFFPEAK',
|
|
||||||
MIDPEAK: 'MIDPEAK',
|
|
||||||
PEAK: 'PEAK'
|
|
||||||
},
|
|
||||||
DIAGNOSTICS: {
|
|
||||||
ENGINE_COOLANT_TEMP: 'ENGINE COOLANT TEMP',
|
|
||||||
ENGINE_RPM: 'ENGINE RPM',
|
|
||||||
LAST_TRIP_FUEL_ECONOMY: 'LAST TRIP FUEL ECONOMY',
|
|
||||||
EV_ESTIMATED_CHARGE_END: 'EV ESTIMATED CHARGE END',
|
|
||||||
EV_BATTERY_LEVEL: 'EV BATTERY LEVEL',
|
|
||||||
OIL_LIFE: 'OIL LIFE',
|
|
||||||
EV_PLUG_VOLTAGE: 'EV PLUG VOLTAGE',
|
|
||||||
LIFETIME_FUEL_ECON: 'LIFETIME FUEL ECON',
|
|
||||||
HOTSPOT_CONFIG: 'HOTSPOT CONFIG',
|
|
||||||
LIFETIME_FUEL_USED: 'LIFETIME FUEL USED',
|
|
||||||
ODOMETER: 'ODOMETER',
|
|
||||||
HOTSPOT_STATUS: 'HOTSPOT STATUS',
|
|
||||||
LIFETIME_EV_ODOMETER: 'LIFETIME EV ODOMETER',
|
|
||||||
EV_PLUG_STATE: 'EV PLUG STATE',
|
|
||||||
EV_CHARGE_STATE: 'EV CHARGE STATE',
|
|
||||||
TIRE_PRESSURE: 'TIRE PRESSURE',
|
|
||||||
AMBIENT_AIR_TEMPERATURE: 'AMBIENT AIR TEMPERATURE',
|
|
||||||
LAST_TRIP_DISTANCE: 'LAST TRIP DISTANCE',
|
|
||||||
INTERM_VOLT_BATT_VOLT: 'INTERM VOLT BATT VOLT',
|
|
||||||
GET_COMMUTE_SCHEDULE: 'GET COMMUTE SCHEDULE',
|
|
||||||
GET_CHARGE_MODE: 'GET CHARGE MODE',
|
|
||||||
EV_SCHEDULED_CHARGE_START: 'EV SCHEDULED CHARGE START',
|
|
||||||
FUEL_TANK_INFO: 'FUEL TANK INFO',
|
|
||||||
HANDS_FREE_CALLING: 'HANDS FREE CALLING',
|
|
||||||
ENERGY_EFFICIENCY: 'ENERGY EFFICIENCY',
|
|
||||||
VEHICLE_RANGE: 'VEHICLE RANGE',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(onstar) {
|
|
||||||
this.onstar = onstar;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAccountVehicles() {
|
|
||||||
return this.onstar.getAccountVehicles();
|
|
||||||
}
|
|
||||||
|
|
||||||
async startVehicle() {
|
|
||||||
return this.onstar.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelStartVehicle() {
|
|
||||||
return this.onstar.cancelStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
async alert({action = [Commands.CONSTANTS.ALERT_ACTION.FLASH],
|
|
||||||
delay = 0, duration = 1, override = []}) {
|
|
||||||
return this.onstar.alert({
|
|
||||||
action,
|
|
||||||
delay,
|
|
||||||
duration,
|
|
||||||
override
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelAlert() {
|
|
||||||
return this.onstar.cancelAlert();
|
|
||||||
}
|
|
||||||
|
|
||||||
async lockDoor({delay = 0}) {
|
|
||||||
return this.onstar.lockDoor({delay});
|
|
||||||
}
|
|
||||||
|
|
||||||
async unlockDoor({delay = 0}) {
|
|
||||||
return this.onstar.unlockDoor({delay});
|
|
||||||
}
|
|
||||||
|
|
||||||
async chargeOverride({mode = Commands.CONSTANTS.CHARGE_OVERRIDE.CHARGE_NOW}) {
|
|
||||||
return this.onstar.chargeOverride({mode});
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelChargeOverride({mode = Commands.CONSTANTS.CHARGE_OVERRIDE.CANCEL_OVERRIDE}) {
|
|
||||||
return this.onstar.chargeOverride({mode});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChargingProfile() {
|
|
||||||
return this.onstar.getChargingProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setChargingProfile() {
|
|
||||||
return this.onstar.setChargingProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLocation() {
|
|
||||||
return this.onstar.location();
|
|
||||||
}
|
|
||||||
|
|
||||||
async diagnostics({diagnosticItem = [
|
|
||||||
Commands.CONSTANTS.DIAGNOSTICS.ODOMETER,
|
|
||||||
Commands.CONSTANTS.DIAGNOSTICS.TIRE_PRESSURE,
|
|
||||||
Commands.CONSTANTS.DIAGNOSTICS.AMBIENT_AIR_TEMPERATURE,
|
|
||||||
Commands.CONSTANTS.DIAGNOSTICS.LAST_TRIP_DISTANCE
|
|
||||||
]}) {
|
|
||||||
return this.onstar.diagnostics({diagnosticItem});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Commands;
|
|
@ -10,9 +10,6 @@ class Diagnostic {
|
|||||||
d => _.has(d, 'value') && _.has(d, 'unit')
|
d => _.has(d, 'value') && _.has(d, 'unit')
|
||||||
);
|
);
|
||||||
this.diagnosticElements = _.map(validEle, e => new DiagnosticElement(e));
|
this.diagnosticElements = _.map(validEle, e => new DiagnosticElement(e));
|
||||||
const converted = _.map(_.filter(this.diagnosticElements, e => e.isConvertible),
|
|
||||||
e => DiagnosticElement.convert(e));
|
|
||||||
this.diagnosticElements.push(... converted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasElements() {
|
hasElements() {
|
||||||
@ -27,29 +24,6 @@ class Diagnostic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DiagnosticElement {
|
class DiagnosticElement {
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {DiagnosticElement} element
|
|
||||||
*/
|
|
||||||
static convert(element) {
|
|
||||||
const {name, unit, value} = element;
|
|
||||||
const convertedUnit = Measurement.convertUnit(unit);
|
|
||||||
return new DiagnosticElement({
|
|
||||||
name: DiagnosticElement.convertName(name, convertedUnit),
|
|
||||||
unit: convertedUnit,
|
|
||||||
value: Measurement.convertValue(value, unit)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static convertName(name, unit) {
|
|
||||||
return `${name} ${_.replace(_.toUpper(unit), /\W/g, '')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} ele.name
|
|
||||||
* @param {string|number} ele.value
|
|
||||||
* @param {string} ele.unit
|
|
||||||
*/
|
|
||||||
constructor(ele) {
|
constructor(ele) {
|
||||||
this._name = ele.name;
|
this._name = ele.name;
|
||||||
this.measurement = new Measurement(ele.value, ele.unit);
|
this.measurement = new Measurement(ele.value, ele.unit);
|
||||||
@ -67,10 +41,6 @@ class DiagnosticElement {
|
|||||||
return this.measurement.unit;
|
return this.measurement.unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isConvertible() {
|
|
||||||
return this.measurement.isConvertible;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `${this.name}: ${this.measurement.toString()}`;
|
return `${this.name}: ${this.measurement.toString()}`;
|
||||||
}
|
}
|
||||||
|
178
src/index.js
178
src/index.js
@ -5,9 +5,6 @@ const _ = require('lodash');
|
|||||||
const Vehicle = require('./vehicle');
|
const Vehicle = require('./vehicle');
|
||||||
const {Diagnostic} = require('./diagnostic');
|
const {Diagnostic} = require('./diagnostic');
|
||||||
const MQTT = require('./mqtt');
|
const MQTT = require('./mqtt');
|
||||||
const Commands = require('./commands');
|
|
||||||
const logger = require('./logger');
|
|
||||||
|
|
||||||
|
|
||||||
const onstarConfig = {
|
const onstarConfig = {
|
||||||
deviceId: process.env.ONSTAR_DEVICEID || uuidv4(),
|
deviceId: process.env.ONSTAR_DEVICEID || uuidv4(),
|
||||||
@ -15,13 +12,9 @@ const onstarConfig = {
|
|||||||
username: process.env.ONSTAR_USERNAME,
|
username: process.env.ONSTAR_USERNAME,
|
||||||
password: process.env.ONSTAR_PASSWORD,
|
password: process.env.ONSTAR_PASSWORD,
|
||||||
onStarPin: process.env.ONSTAR_PIN,
|
onStarPin: process.env.ONSTAR_PIN,
|
||||||
checkRequestStatus: _.get(process.env, 'ONSTAR_SYNC', 'true') === 'true',
|
checkRequestStatus: process.env.ONSTAR_SYNC === "true" || true,
|
||||||
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000), // 30 min
|
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000) // 30 min
|
||||||
requestPollingIntervalSeconds: parseInt(process.env.ONSTAR_POLL_INTERVAL) || 6, // 6 sec default
|
|
||||||
requestPollingTimeoutSeconds: parseInt(process.env.ONSTAR_POLL_TIMEOUT) || 60, // 60 sec default
|
|
||||||
allowCommands: _.get(process.env, 'ONSTAR_ALLOW_COMMANDS', 'true') === 'true'
|
|
||||||
};
|
};
|
||||||
logger.info('OnStar Config', {onstarConfig});
|
|
||||||
|
|
||||||
const mqttConfig = {
|
const mqttConfig = {
|
||||||
host: process.env.MQTT_HOST || 'localhost',
|
host: process.env.MQTT_HOST || 'localhost',
|
||||||
@ -30,165 +23,66 @@ const mqttConfig = {
|
|||||||
port: parseInt(process.env.MQTT_PORT) || 1883,
|
port: parseInt(process.env.MQTT_PORT) || 1883,
|
||||||
tls: process.env.MQTT_TLS || false,
|
tls: process.env.MQTT_TLS || false,
|
||||||
prefix: process.env.MQTT_PREFIX || 'homeassistant',
|
prefix: process.env.MQTT_PREFIX || 'homeassistant',
|
||||||
namePrefix: process.env.MQTT_NAME_PREFIX || '',
|
|
||||||
};
|
};
|
||||||
logger.info('MQTT Config', {mqttConfig});
|
|
||||||
|
|
||||||
const init = () => new Commands(OnStar.create(onstarConfig));
|
let loop;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const onStar = OnStar.create(onstarConfig);
|
||||||
|
const client = await mqtt.connectAsync(`${mqttConfig.tls
|
||||||
|
? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`,
|
||||||
|
{ username: mqttConfig.username, password: mqttConfig.password });
|
||||||
|
|
||||||
const getVehicles = async commands => {
|
console.log('Requesting vehicles.');
|
||||||
logger.info('Requesting vehicles');
|
const vehiclesRes = await onStar.getAccountVehicles();
|
||||||
const vehiclesRes = await commands.getAccountVehicles();
|
console.log(_.get(vehiclesRes, 'status'));
|
||||||
logger.info('Vehicle request status', {status: _.get(vehiclesRes, 'status')});
|
|
||||||
const vehicles = _.map(
|
const vehicles = _.map(
|
||||||
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
|
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
|
||||||
v => new Vehicle(v)
|
v => new Vehicle(v)
|
||||||
);
|
);
|
||||||
logger.debug('Vehicle request response', {vehicles: _.map(vehicles, v => v.toString())});
|
console.log('Vehicles returned:');
|
||||||
return vehicles;
|
for (const v of vehicles) {
|
||||||
|
console.log(v.toString());
|
||||||
}
|
}
|
||||||
|
const mqttHA = new MQTT('homeassistant', vehicles[0].vin);
|
||||||
|
|
||||||
const getCurrentVehicle = async commands => {
|
|
||||||
const vehicles = await getVehicles(commands);
|
|
||||||
const currentVeh = _.find(vehicles, v => v.vin.toLowerCase() === onstarConfig.vin.toLowerCase());
|
|
||||||
if (!currentVeh) {
|
|
||||||
throw new Error(`Configured vehicle VIN ${onstarConfig.vin} not available in account vehicles`);
|
|
||||||
}
|
|
||||||
return currentVeh;
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectMQTT = async availabilityTopic => {
|
|
||||||
const url = `${mqttConfig.tls ? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`;
|
|
||||||
const config = {
|
|
||||||
username: mqttConfig.username,
|
|
||||||
password: mqttConfig.password,
|
|
||||||
will: {topic: availabilityTopic, payload: 'false', retain: true}
|
|
||||||
};
|
|
||||||
logger.info('Connecting to MQTT', {url, config: _.omit(config, 'password')});
|
|
||||||
const client = await mqtt.connectAsync(url, config);
|
|
||||||
logger.info('Connected to MQTT');
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configureMQTT = async (commands, client, mqttHA) => {
|
|
||||||
if (!onstarConfig.allowCommands)
|
|
||||||
return;
|
|
||||||
|
|
||||||
client.on('message', (topic, message) => {
|
|
||||||
logger.debug('Subscription message', {topic, message});
|
|
||||||
const {command, options} = JSON.parse(message);
|
|
||||||
const cmd = commands[command];
|
|
||||||
if (!cmd) {
|
|
||||||
logger.error('Command not found', {command});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const commandFn = cmd.bind(commands);
|
|
||||||
logger.info('Command sent', { command });
|
|
||||||
commandFn(options || {})
|
|
||||||
.then(data => {
|
|
||||||
// TODO refactor the response handling for commands
|
|
||||||
logger.info('Command completed', { command });
|
|
||||||
const responseData = _.get(data, 'response.data');
|
|
||||||
if (responseData) {
|
|
||||||
logger.info('Command response data', { responseData });
|
|
||||||
const location = _.get(data, 'response.data.commandResponse.body.location');
|
|
||||||
if (location) {
|
|
||||||
const topic = mqttHA.getStateTopic({ name: command });
|
|
||||||
// TODO create device_tracker entity. MQTT device tracker doesn't support lat/lon and mqtt_json
|
|
||||||
// doesn't have discovery
|
|
||||||
client.publish(topic,
|
|
||||||
JSON.stringify({ latitude: location.lat, longitude: location.long }), { retain: true })
|
|
||||||
.then(() => logger.info('Published location to topic.', { topic }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err=> logger.error('Command error', {command, err}));
|
|
||||||
});
|
|
||||||
const topic = mqttHA.getCommandTopic();
|
|
||||||
logger.info('Subscribed to command topic', {topic});
|
|
||||||
await client.subscribe(topic);
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const commands = init();
|
|
||||||
const vehicle = await getCurrentVehicle(commands);
|
|
||||||
|
|
||||||
const mqttHA = new MQTT(vehicle, mqttConfig.prefix, mqttConfig.namePrefix);
|
|
||||||
const availTopic = mqttHA.getAvailabilityTopic();
|
|
||||||
const client = await connectMQTT(availTopic);
|
|
||||||
client.publish(availTopic, 'true', {retain: true})
|
|
||||||
.then(() => logger.debug('Published availability'));
|
|
||||||
await configureMQTT(commands, client, mqttHA);
|
|
||||||
|
|
||||||
const configurations = new Map();
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const states = new Map();
|
// Note: the library is set to use only the configured VIN, but using multiple for future proofing.
|
||||||
const v = vehicle;
|
for (const v of vehicles) {
|
||||||
logger.info('Requesting diagnostics');
|
console.log('Requesting diagnostics:')
|
||||||
const statsRes = await commands.diagnostics({diagnosticItem: v.getSupported()});
|
const statsRes = await onStar.diagnostics({
|
||||||
logger.info('Diagnostic request status', {status: _.get(statsRes, 'status')});
|
diagnosticItem: v.getSupported()
|
||||||
|
});
|
||||||
|
console.log(_.get(statsRes, 'status'));
|
||||||
const stats = _.map(
|
const stats = _.map(
|
||||||
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
|
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
|
||||||
d => new Diagnostic(d)
|
d => new Diagnostic(d)
|
||||||
);
|
);
|
||||||
logger.debug('Diagnostic request response', {stats: _.map(stats, s => s.toString())});
|
|
||||||
|
|
||||||
for (const s of stats) {
|
for (const s of stats) {
|
||||||
if (!s.hasElements()) {
|
if (!s.hasElements()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// configure once, then set or update states
|
// configure, then set state
|
||||||
for (const d of s.diagnosticElements) {
|
for (const d of s.diagnosticElements) {
|
||||||
const topic = mqttHA.getConfigTopic(d)
|
console.log(mqttHA.getConfigTopic(d));
|
||||||
const payload = mqttHA.getConfigPayload(s, d);
|
console.log(JSON.stringify(mqttHA.getConfigPayload(s, d)));
|
||||||
configurations.set(topic, {configured: false, payload});
|
await client.publish(mqttHA.getConfigTopic(d), JSON.stringify(mqttHA.getConfigPayload(s, d)));
|
||||||
}
|
}
|
||||||
|
console.log(mqttHA.getStateTopic(s));
|
||||||
const topic = mqttHA.getStateTopic(s);
|
console.log(JSON.stringify(mqttHA.getStatePayload(s)));
|
||||||
const payload = mqttHA.getStatePayload(s);
|
await client.publish(mqttHA.getStateTopic(s), JSON.stringify(mqttHA.getStatePayload(s)));
|
||||||
states.set(topic, payload);
|
|
||||||
}
|
|
||||||
const publishes = [];
|
|
||||||
// publish sensor configs
|
|
||||||
for (let [topic, config] of configurations) {
|
|
||||||
// configure once
|
|
||||||
if (!config.configured) {
|
|
||||||
config.configured = true;
|
|
||||||
const {payload} = config;
|
|
||||||
logger.info('Publishing message', {topic, payload});
|
|
||||||
publishes.push(
|
|
||||||
client.publish(topic, JSON.stringify(payload), {retain: true})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update sensor states
|
|
||||||
for (let [topic, state] of states) {
|
|
||||||
logger.info('Publishing message', {topic, state});
|
|
||||||
publishes.push(
|
|
||||||
client.publish(topic, JSON.stringify(state), {retain: true})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await Promise.all(publishes);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => run()
|
const main = () => run()
|
||||||
.then(() => logger.info('Updates complete, sleeping.'))
|
.then(() => console.log('Done, sleeping.'))
|
||||||
.catch(e => {
|
.catch(e => console.error(e))
|
||||||
if (e instanceof Error) {
|
|
||||||
logger.error('Error', {error: _.pick(e, [
|
|
||||||
'message', 'stack',
|
|
||||||
'response.status', 'response.statusText', 'response.headers', 'response.data',
|
|
||||||
'request.method', 'request.body', 'request.contentType', 'request.headers', 'request.url'
|
|
||||||
])});
|
|
||||||
} else {
|
|
||||||
logger.error('Error', {error: e});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await main();
|
main();
|
||||||
setInterval(main, onstarConfig.refreshInterval);
|
loop = setInterval(main, onstarConfig.refreshInterval);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Main function error.', {error: e});
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
@ -1,12 +0,0 @@
|
|||||||
const winston = require('winston');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
|
||||||
level: _.get(process, 'env.LOG_LEVEL', 'info'),
|
|
||||||
format: winston.format.simple(),
|
|
||||||
// format: winston.format.json(),
|
|
||||||
transports: [new winston.transports.Console({stderrLevels: ['error']})]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = logger;
|
|
@ -1,20 +1,9 @@
|
|||||||
const _ = require('lodash');
|
// const convert = require('convert-units');
|
||||||
const convert = require('convert-units');
|
|
||||||
|
|
||||||
class Measurement {
|
class Measurement {
|
||||||
static CONVERTABLE_UNITS = [
|
|
||||||
'°C',
|
|
||||||
'km',
|
|
||||||
'kPa',
|
|
||||||
'km/l(e)',
|
|
||||||
// Helps with conversion to Gallons.
|
|
||||||
'lit'
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(value, unit) {
|
constructor(value, unit) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.unit = Measurement.correctUnitName(unit);
|
this.unit = Measurement.correctUnitName(unit);
|
||||||
this.isConvertible = _.includes(Measurement.CONVERTABLE_UNITS, this.unit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,12 +22,10 @@ class Measurement {
|
|||||||
case 'KPa':
|
case 'KPa':
|
||||||
return 'kPa';
|
return 'kPa';
|
||||||
case 'kmple':
|
case 'kmple':
|
||||||
return 'km/l(e)';
|
return 'km/l(e)'; // TODO check on this
|
||||||
case 'volts':
|
case 'volts':
|
||||||
case 'Volts':
|
case 'Volts':
|
||||||
return 'V';
|
return 'V';
|
||||||
case 'l':
|
|
||||||
return 'lit';
|
|
||||||
// these are states
|
// these are states
|
||||||
case 'Stat':
|
case 'Stat':
|
||||||
case 'N/A':
|
case 'N/A':
|
||||||
@ -49,55 +36,16 @@ class Measurement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// TODO this may not be required. Check consuming application.
|
||||||
*
|
/*static convertToImperial(value, unit) {
|
||||||
* @param {string|number} value
|
|
||||||
* @param {string} unit
|
|
||||||
* @returns {string|number}
|
|
||||||
*/
|
|
||||||
static convertValue(value, unit) {
|
|
||||||
switch(unit) {
|
switch(unit) {
|
||||||
case '°C':
|
case 'Cel':
|
||||||
value = _.round(convert(value).from('C').to('F'));
|
const val = convert(value).from('C').to('F');
|
||||||
break;
|
return new Measurement(val, 'F');
|
||||||
case 'km':
|
|
||||||
value = _.round(convert(value).from('km').to('mi'), 1);
|
|
||||||
break;
|
|
||||||
case 'kPa':
|
|
||||||
value = _.round(convert(value).from('kPa').to('psi'), 1);
|
|
||||||
break;
|
|
||||||
case 'km/l(e)':
|
|
||||||
// km/L = (1.609344 / 3.785411784) * MPG
|
|
||||||
value = _.round(value / (1.609344 / 3.785411784), 1);
|
|
||||||
break;
|
|
||||||
case 'lit':
|
|
||||||
value = _.round(value / 3.785411784, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} unit
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
static convertUnit(unit) {
|
|
||||||
switch (unit) {
|
|
||||||
case '°C':
|
|
||||||
return '°F';
|
|
||||||
case 'km':
|
|
||||||
return 'mi';
|
|
||||||
case 'kPa':
|
|
||||||
return 'psi';
|
|
||||||
case 'km/l(e)':
|
|
||||||
return 'mpg(e)';
|
|
||||||
case 'lit':
|
|
||||||
return 'gal';
|
|
||||||
default:
|
default:
|
||||||
return unit;
|
return new Measurement(value, unit);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `${this.value}${this.unit}`;
|
return `${this.value}${this.unit}`;
|
||||||
|
97
src/mqtt.js
97
src/mqtt.js
@ -36,11 +36,9 @@ const _ = require('lodash');
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
class MQTT {
|
class MQTT {
|
||||||
constructor(vehicle, prefix = 'homeassistant', namePrefix) {
|
constructor(prefix = 'homeassistant', instance = 'XXX') {
|
||||||
this.prefix = prefix;
|
this.prefix = prefix;
|
||||||
this.vehicle = vehicle;
|
this.instance = instance;
|
||||||
this.instance = vehicle.vin;
|
|
||||||
this.namePrefix = namePrefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static convertName(name) {
|
static convertName(name) {
|
||||||
@ -58,38 +56,19 @@ class MQTT {
|
|||||||
case 'PRIORITY CHARGE INDICATOR':
|
case 'PRIORITY CHARGE INDICATOR':
|
||||||
case 'PRIORITY CHARGE STATUS':
|
case 'PRIORITY CHARGE STATUS':
|
||||||
return 'binary_sensor';
|
return 'binary_sensor';
|
||||||
case 'getLocation':
|
|
||||||
return 'device_tracker';
|
|
||||||
default:
|
default:
|
||||||
return 'sensor';
|
return 'sensor';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} name
|
* @param {'sensor'|'binary_sensor'} type
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
addNamePrefix(name) {
|
|
||||||
if (!this.namePrefix) return name
|
|
||||||
return `${this.namePrefix} ${name}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {'sensor'|'binary_sensor'|'device_tracker'} type
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getBaseTopic(type = 'sensor') {
|
getBaseTopic(type = 'sensor') {
|
||||||
return `${this.prefix}/${type}/${this.instance}`;
|
return `${this.prefix}/${type}/${this.instance}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailabilityTopic() {
|
|
||||||
return `${this.prefix}/${this.instance}/available`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommandTopic() {
|
|
||||||
return `${this.prefix}/${this.instance}/command`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {DiagnosticElement} diag
|
* @param {DiagnosticElement} diag
|
||||||
@ -141,7 +120,6 @@ class MQTT {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// coerce to number if possible, API uses strings :eyeroll:
|
// coerce to number if possible, API uses strings :eyeroll:
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const num = _.toNumber(e.value);
|
const num = _.toNumber(e.value);
|
||||||
value = _.isNaN(num) ? e.value : num;
|
value = _.isNaN(num) ? e.value : num;
|
||||||
break;
|
break;
|
||||||
@ -151,46 +129,19 @@ class MQTT {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
mapBaseConfigPayload(diag, diagEl, device_class, name, attr) {
|
mapConfigPayload(diag, diagEl, device_class, name, attr) {
|
||||||
name = name || MQTT.convertFriendlyName(diagEl.name);
|
name = name || MQTT.convertFriendlyName(diagEl.name);
|
||||||
name = this.addNamePrefix(name);
|
// TODO availability
|
||||||
// Generate the unique id from the vin and name
|
|
||||||
let unique_id = `${this.vehicle.vin}-${diagEl.name}`
|
|
||||||
unique_id = unique_id.replace(/\s+/g, '-').toLowerCase();
|
|
||||||
return {
|
return {
|
||||||
device_class,
|
device_class,
|
||||||
name,
|
name,
|
||||||
device: {
|
|
||||||
identifiers: [this.vehicle.vin],
|
|
||||||
manufacturer: this.vehicle.make,
|
|
||||||
model: this.vehicle.year,
|
|
||||||
name: this.vehicle.toString()
|
|
||||||
},
|
|
||||||
availability_topic: this.getAvailabilityTopic(),
|
|
||||||
payload_available: 'true',
|
|
||||||
payload_not_available: 'false',
|
|
||||||
state_topic: this.getStateTopic(diag),
|
state_topic: this.getStateTopic(diag),
|
||||||
|
unit_of_measurement: diagEl.unit,
|
||||||
value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`,
|
value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`,
|
||||||
json_attributes_topic: _.isUndefined(attr) ? undefined : this.getStateTopic(diag),
|
json_attributes_template: attr
|
||||||
json_attributes_template: attr,
|
|
||||||
unique_id: unique_id
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mapSensorConfigPayload(diag, diagEl, device_class, name, attr) {
|
|
||||||
name = name || MQTT.convertFriendlyName(diagEl.name);
|
|
||||||
return _.extend(
|
|
||||||
this.mapBaseConfigPayload(diag, diagEl, device_class, name, attr),
|
|
||||||
{unit_of_measurement: diagEl.unit});
|
|
||||||
}
|
|
||||||
|
|
||||||
mapBinarySensorConfigPayload(diag, diagEl, device_class, name, attr) {
|
|
||||||
name = name || MQTT.convertFriendlyName(diagEl.name);
|
|
||||||
return _.extend(
|
|
||||||
this.mapBaseConfigPayload(diag, diagEl, device_class, name, attr),
|
|
||||||
{payload_on: true, payload_off: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Diagnostic} diag
|
* @param {Diagnostic} diag
|
||||||
@ -202,43 +153,31 @@ class MQTT {
|
|||||||
case 'LIFETIME ENERGY USED':
|
case 'LIFETIME ENERGY USED':
|
||||||
case 'LIFETIME EFFICIENCY':
|
case 'LIFETIME EFFICIENCY':
|
||||||
case 'ELECTRIC ECONOMY':
|
case 'ELECTRIC ECONOMY':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'energy');
|
return this.mapConfigPayload(diag, diagEl, 'energy');
|
||||||
case 'INTERM VOLT BATT VOLT':
|
case 'INTERM VOLT BATT VOLT':
|
||||||
case 'EV PLUG VOLTAGE':
|
case 'EV PLUG VOLTAGE':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'voltage');
|
return this.mapConfigPayload(diag, diagEl, 'voltage');
|
||||||
case 'HYBRID BATTERY MINIMUM TEMPERATURE':
|
case 'HYBRID BATTERY MINIMUM TEMPERATURE':
|
||||||
case 'AMBIENT AIR TEMPERATURE':
|
case 'AMBIENT AIR TEMPERATURE':
|
||||||
case 'AMBIENT AIR TEMPERATURE F':
|
return this.mapConfigPayload(diag, diagEl, 'temperature');
|
||||||
case 'ENGINE COOLANT TEMP':
|
|
||||||
case 'ENGINE COOLANT TEMP F':
|
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'temperature');
|
|
||||||
case 'EV BATTERY LEVEL':
|
case 'EV BATTERY LEVEL':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'battery');
|
return this.mapConfigPayload(diag, diagEl, 'battery');
|
||||||
case 'TIRE PRESSURE LF':
|
case 'TIRE PRESSURE LF':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
|
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}");
|
||||||
case 'TIRE PRESSURE LF PSI':
|
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT_PSI')}} | tojson }}`);
|
|
||||||
case 'TIRE PRESSURE LR':
|
case 'TIRE PRESSURE LR':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
|
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}");
|
||||||
case 'TIRE PRESSURE LR PSI':
|
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR_PSI')}} | tojson }}`);
|
|
||||||
case 'TIRE PRESSURE RF':
|
case 'TIRE PRESSURE RF':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
|
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}");
|
||||||
case 'TIRE PRESSURE RF PSI':
|
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT_PSI')}} | tojson }}`);
|
|
||||||
case 'TIRE PRESSURE RR':
|
case 'TIRE PRESSURE RR':
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
|
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}");
|
||||||
case 'TIRE PRESSURE RR PSI':
|
|
||||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR_PSI')}} | tojson }}`);
|
|
||||||
// binary sensor
|
// binary sensor
|
||||||
case 'EV PLUG STATE': // unplugged/plugged
|
case 'EV PLUG STATE': // unplugged/plugged
|
||||||
return this.mapBinarySensorConfigPayload(diag, diagEl, 'plug');
|
return this.mapConfigPayload(diag, diagEl, 'plug');
|
||||||
case 'EV CHARGE STATE': // not_charging/charging
|
case 'EV CHARGE STATE': // not_charging/charging
|
||||||
return this.mapBinarySensorConfigPayload(diag, diagEl, 'battery_charging');
|
return this.mapConfigPayload(diag, diagEl, 'battery_charging');
|
||||||
// binary_sensor, but no applicable device_class
|
// binary_sensor, but no applicable device_class
|
||||||
case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE
|
case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE
|
||||||
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
|
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
|
||||||
return this.mapBinarySensorConfigPayload(diag, diagEl);
|
|
||||||
// no device class, camel case name
|
// no device class, camel case name
|
||||||
case 'EV RANGE':
|
case 'EV RANGE':
|
||||||
case 'ODOMETER':
|
case 'ODOMETER':
|
||||||
@ -247,7 +186,7 @@ class MQTT {
|
|||||||
case 'LIFETIME MPGE':
|
case 'LIFETIME MPGE':
|
||||||
case 'CHARGER POWER LEVEL':
|
case 'CHARGER POWER LEVEL':
|
||||||
default:
|
default:
|
||||||
return this.mapSensorConfigPayload(diag, diagEl);
|
return this.mapConfigPayload(diag, diagEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class Vehicle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `${this.year} ${this.make} ${this.model}`;
|
return `${this.year} ${this.make} ${this.model} ${this.vin}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const { Diagnostic, DiagnosticElement } = require('../src/diagnostic');
|
const { Diagnostic } = require('../src/diagnostic');
|
||||||
const apiResponse = require('./diagnostic.sample.json');
|
const apiResponse = require('./diagnostic.sample.json');
|
||||||
|
|
||||||
describe('Diagnostics', () => {
|
describe('Diagnostics', () => {
|
||||||
@ -12,13 +12,13 @@ describe('Diagnostics', () => {
|
|||||||
|
|
||||||
it('should parse a diagnostic response', () => {
|
it('should parse a diagnostic response', () => {
|
||||||
assert.strictEqual(d.name, 'AMBIENT AIR TEMPERATURE');
|
assert.strictEqual(d.name, 'AMBIENT AIR TEMPERATURE');
|
||||||
assert.strictEqual(d.diagnosticElements.length, 2);
|
assert.strictEqual(d.diagnosticElements.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should toString() correctly', () => {
|
it('should toString() correctly', () => {
|
||||||
const output = d.toString().trimEnd();
|
const output = d.toString().trimEnd();
|
||||||
const lines = output.split(/\r\n|\r|\n/);
|
const lines = output.split(/\r\n|\r|\n/);
|
||||||
assert.strictEqual(lines.length, 3);
|
assert.strictEqual(lines.length, 2);
|
||||||
assert.strictEqual(lines[0], 'AMBIENT AIR TEMPERATURE:');
|
assert.strictEqual(lines[0], 'AMBIENT AIR TEMPERATURE:');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -28,19 +28,15 @@ describe('Diagnostics', () => {
|
|||||||
it('should parse a diagnostic element', () => {
|
it('should parse a diagnostic element', () => {
|
||||||
assert.strictEqual(d.name, 'TIRE PRESSURE');
|
assert.strictEqual(d.name, 'TIRE PRESSURE');
|
||||||
assert.ok(_.isArray(d.diagnosticElements));
|
assert.ok(_.isArray(d.diagnosticElements));
|
||||||
assert.strictEqual(d.diagnosticElements.length, 12);
|
assert.strictEqual(d.diagnosticElements.length, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should toString() correctly', () => {
|
it('should toString() correctly', () => {
|
||||||
const output = d.toString().trimEnd();
|
const output = d.toString().trimEnd();
|
||||||
const lines = output.split(/\r\n|\r|\n/);
|
const lines = output.split(/\r\n|\r|\n/);
|
||||||
assert.strictEqual(lines.length, 13);
|
assert.strictEqual(lines.length, 7);
|
||||||
assert.strictEqual(lines[0], 'TIRE PRESSURE:');
|
assert.strictEqual(lines[0], 'TIRE PRESSURE:');
|
||||||
assert.strictEqual(lines[1], ' TIRE PRESSURE LF: 240.0kPa');
|
assert.strictEqual(lines[1], ' TIRE PRESSURE LF: 240.0kPa');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should strip non-alpha chars', () => {
|
|
||||||
assert.strictEqual(DiagnosticElement.convertName('TEMP', '°F'), 'TEMP F');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,13 +3,11 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const { Diagnostic } = require('../src/diagnostic');
|
const { Diagnostic } = require('../src/diagnostic');
|
||||||
const MQTT = require('../src/mqtt');
|
const MQTT = require('../src/mqtt');
|
||||||
const Vehicle = require('../src/vehicle');
|
|
||||||
const apiResponse = require('./diagnostic.sample.json');
|
const apiResponse = require('./diagnostic.sample.json');
|
||||||
|
|
||||||
describe('MQTT', () => {
|
describe('MQTT', () => {
|
||||||
let mqtt;
|
let mqtt;
|
||||||
let vehicle = new Vehicle({make: 'foo', model: 'bar', vin: 'XXX', year: 2020});
|
beforeEach(() => mqtt = new MQTT());
|
||||||
beforeEach(() => mqtt = new MQTT(vehicle));
|
|
||||||
|
|
||||||
it('should set defaults', () => {
|
it('should set defaults', () => {
|
||||||
assert.strictEqual(mqtt.prefix, 'homeassistant');
|
assert.strictEqual(mqtt.prefix, 'homeassistant');
|
||||||
@ -28,27 +26,8 @@ describe('MQTT', () => {
|
|||||||
assert.strictEqual(MQTT.convertFriendlyName('FOO BAR'), 'Foo Bar');
|
assert.strictEqual(MQTT.convertFriendlyName('FOO BAR'), 'Foo Bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should determine sensor types', () => {
|
|
||||||
assert.strictEqual(MQTT.determineSensorType('EV CHARGE STATE'), 'binary_sensor');
|
|
||||||
assert.strictEqual(MQTT.determineSensorType('EV PLUG STATE'), 'binary_sensor');
|
|
||||||
assert.strictEqual(MQTT.determineSensorType('PRIORITY CHARGE INDICATOR'), 'binary_sensor');
|
|
||||||
assert.strictEqual(MQTT.determineSensorType('PRIORITY CHARGE STATUS'), 'binary_sensor');
|
|
||||||
assert.strictEqual(MQTT.determineSensorType('getLocation'), 'device_tracker');
|
|
||||||
assert.strictEqual(MQTT.determineSensorType('foo'), 'sensor');
|
|
||||||
assert.strictEqual(MQTT.determineSensorType(''), 'sensor');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('topics', () => {
|
describe('topics', () => {
|
||||||
let d;
|
let d;
|
||||||
|
|
||||||
it('should generate availability topic', () => {
|
|
||||||
assert.strictEqual(mqtt.getAvailabilityTopic(), 'homeassistant/XXX/available');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate command topic', () => {
|
|
||||||
assert.strictEqual(mqtt.getCommandTopic(), 'homeassistant/XXX/command');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sensor', () => {
|
describe('sensor', () => {
|
||||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
|
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
|
||||||
|
|
||||||
@ -56,7 +35,7 @@ describe('MQTT', () => {
|
|||||||
assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config');
|
assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config');
|
||||||
});
|
});
|
||||||
it('should generate state topics', () => {
|
it('should generate state topics', () => {
|
||||||
assert.strictEqual(mqtt.getStateTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/state');
|
assert.strictEqual(mqtt.getStateTopic(d, d.diagnosticElements[0]), 'homeassistant/sensor/XXX/ambient_air_temperature/state');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,52 +56,17 @@ describe('MQTT', () => {
|
|||||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
|
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
|
||||||
it('should generate config payloads', () => {
|
it('should generate config payloads', () => {
|
||||||
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), {
|
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), {
|
||||||
availability_topic: 'homeassistant/XXX/available',
|
|
||||||
device: {
|
|
||||||
identifiers: [
|
|
||||||
'XXX'
|
|
||||||
],
|
|
||||||
manufacturer: 'foo',
|
|
||||||
model: 2020,
|
|
||||||
name: '2020 foo bar'
|
|
||||||
},
|
|
||||||
device_class: 'temperature',
|
device_class: 'temperature',
|
||||||
json_attributes_template: undefined,
|
json_attributes_template: undefined,
|
||||||
name: 'Ambient Air Temperature',
|
name: 'Ambient Air Temperature',
|
||||||
payload_available: 'true',
|
|
||||||
payload_not_available: 'false',
|
|
||||||
state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state',
|
state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state',
|
||||||
unique_id: 'xxx-ambient-air-temperature',
|
|
||||||
json_attributes_topic: undefined,
|
|
||||||
unit_of_measurement: '°C',
|
unit_of_measurement: '°C',
|
||||||
value_template: '{{ value_json.ambient_air_temperature }}'
|
value_template: '{{ value_json.ambient_air_temperature }}'
|
||||||
});
|
});
|
||||||
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), {
|
|
||||||
availability_topic: 'homeassistant/XXX/available',
|
|
||||||
device: {
|
|
||||||
identifiers: [
|
|
||||||
'XXX'
|
|
||||||
],
|
|
||||||
manufacturer: 'foo',
|
|
||||||
model: 2020,
|
|
||||||
name: '2020 foo bar'
|
|
||||||
},
|
|
||||||
device_class: 'temperature',
|
|
||||||
json_attributes_template: undefined,
|
|
||||||
name: 'Ambient Air Temperature F',
|
|
||||||
payload_available: 'true',
|
|
||||||
payload_not_available: 'false',
|
|
||||||
state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state',
|
|
||||||
unique_id: 'xxx-ambient-air-temperature-f',
|
|
||||||
json_attributes_topic: undefined,
|
|
||||||
unit_of_measurement: '°F',
|
|
||||||
value_template: '{{ value_json.ambient_air_temperature_f }}'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
it('should generate state payloads', () => {
|
it('should generate state payloads', () => {
|
||||||
assert.deepStrictEqual(mqtt.getStatePayload(d), {
|
assert.deepStrictEqual(mqtt.getStatePayload(d), {
|
||||||
ambient_air_temperature: 15,
|
ambient_air_temperature: 15
|
||||||
ambient_air_temperature_f: 59
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -131,25 +75,11 @@ describe('MQTT', () => {
|
|||||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]')));
|
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]')));
|
||||||
it('should generate config payloads', () => {
|
it('should generate config payloads', () => {
|
||||||
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), {
|
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), {
|
||||||
availability_topic: 'homeassistant/XXX/available',
|
|
||||||
device: {
|
|
||||||
identifiers: [
|
|
||||||
'XXX'
|
|
||||||
],
|
|
||||||
manufacturer: 'foo',
|
|
||||||
model: 2020,
|
|
||||||
name: '2020 foo bar'
|
|
||||||
},
|
|
||||||
device_class: undefined,
|
device_class: undefined,
|
||||||
json_attributes_template: undefined,
|
json_attributes_template: undefined,
|
||||||
name: 'Priority Charge Indicator',
|
name: 'Priority Charge Indicator',
|
||||||
payload_available: 'true',
|
|
||||||
payload_not_available: 'false',
|
|
||||||
payload_off: false,
|
|
||||||
payload_on: true,
|
|
||||||
state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state',
|
state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state',
|
||||||
unique_id: 'xxx-priority-charge-indicator',
|
unit_of_measurement: undefined,
|
||||||
json_attributes_topic: undefined,
|
|
||||||
value_template: '{{ value_json.priority_charge_indicator }}'
|
value_template: '{{ value_json.priority_charge_indicator }}'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -161,32 +91,5 @@ describe('MQTT', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('attributes', () => {
|
|
||||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]')));
|
|
||||||
it('should generate payloads with an attribute', () => {
|
|
||||||
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), {
|
|
||||||
availability_topic: 'homeassistant/XXX/available',
|
|
||||||
device: {
|
|
||||||
identifiers: [
|
|
||||||
'XXX'
|
|
||||||
],
|
|
||||||
manufacturer: 'foo',
|
|
||||||
model: 2020,
|
|
||||||
name: '2020 foo bar'
|
|
||||||
},
|
|
||||||
device_class: 'pressure',
|
|
||||||
json_attributes_template: "{{ {'recommendation': value_json.tire_pressure_placard_front} | tojson }}",
|
|
||||||
name: 'Tire Pressure: Left Front',
|
|
||||||
payload_available: 'true',
|
|
||||||
payload_not_available: 'false',
|
|
||||||
state_topic: 'homeassistant/sensor/XXX/tire_pressure/state',
|
|
||||||
unique_id: 'xxx-tire-pressure-lf',
|
|
||||||
json_attributes_topic: 'homeassistant/sensor/XXX/tire_pressure/state',
|
|
||||||
unit_of_measurement: 'kPa',
|
|
||||||
value_template: '{{ value_json.tire_pressure_lf }}'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -36,6 +36,6 @@ describe('Vehicle', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should toString() correctly', () => {
|
it('should toString() correctly', () => {
|
||||||
assert.strictEqual(v.toString(), '2020 Chevrolet Bolt EV')
|
assert.strictEqual(v.toString(), '2020 Chevrolet Bolt EV foobarVIN')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user