Compare commits
352 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f69adb279e | ||
|
80bd465c74 | ||
|
c59295df84 | ||
|
042bec5ac5 | ||
|
5bc49c968e | ||
|
f1c167668a | ||
|
6fb09602d7 | ||
|
8b5826f345 | ||
|
44953eb1aa | ||
|
6a73b1c9b9 | ||
|
daca343fef | ||
|
6e65ee3afd | ||
|
46ffd940d7 | ||
|
5c7ec42b1f | ||
|
71e537aed3 | ||
|
038141060f | ||
|
397a798121 | ||
|
c678cab6b4 | ||
|
3bfbb27848 | ||
|
660465e9bf | ||
|
65e4373b52 | ||
|
c5fe4ed024 | ||
|
ca2b7775fc | ||
|
5a1eab2df5 | ||
|
e791f6e13d | ||
|
4d754269df | ||
|
e7bf57b210 | ||
|
540f345e47 | ||
|
500e6e22d8 | ||
|
ad3f292135 | ||
|
ab4e5c7f24 | ||
|
7f48f6b356 | ||
|
eaf8ce7764 | ||
|
6316c479a1 | ||
|
ad5348c967 | ||
|
877a16c2f7 | ||
|
39df6a23bf | ||
|
efa60e2f23 | ||
|
6ba7a200e1 | ||
|
3713f49ed8 | ||
|
348ae8c76c | ||
|
f653c8a9a7 | ||
|
d95357fd65 | ||
|
fb2f52a110 | ||
|
b3b2050bc0 | ||
|
77b41375f8 | ||
|
43e5b8c652 | ||
|
652fb16288 | ||
|
e51ea1502a | ||
|
8ee3a65bf6 | ||
|
7e374a900e | ||
|
aec1c05e51 | ||
|
0259d7dd51 | ||
|
7ab093cd00 | ||
|
13507bce50 | ||
|
2e1faf0abb | ||
|
8c6f58e940 | ||
|
643320876d | ||
|
d9eb6839da | ||
|
fbef114e61 | ||
|
17a447438f | ||
|
c8c0979e15 | ||
|
eea3a40eae | ||
|
665dc4c443 | ||
|
51c3e1a5f8 | ||
|
4fb94f8d56 | ||
|
09b0180ca9 | ||
|
90710d353b | ||
|
a5ce42ec8f | ||
|
63ac1c0c06 | ||
|
b4ecca4681 | ||
|
7248e59d06 | ||
|
c6d815d478 | ||
|
4887a865c0 | ||
|
e427908b15 | ||
|
20b57318bc | ||
|
cebef8b003 | ||
|
c84ada7738 | ||
|
0e684514c0 | ||
|
59ffc992f2 | ||
|
a4a9b90a3c | ||
|
1e903085d1 | ||
|
37a776195c | ||
|
5b66c55e44 | ||
|
3958014f11 | ||
|
9cf676b7d0 | ||
|
a539375ae5 | ||
|
2e5f9621f8 | ||
|
9a5daf33c7 | ||
|
b6812d377b | ||
|
37e294330f | ||
|
ae56234956 | ||
|
f0574fdd1d | ||
|
d918f946ca | ||
|
d3128f57e2 | ||
|
a435e6cfbb | ||
|
af700aa3fa | ||
|
34ae1d4aa0 | ||
|
ff89a218b8 | ||
|
edb6457fcc | ||
|
c57048f2d0 | ||
|
ae35bf3851 | ||
|
cd35218512 | ||
|
4f7f8f8193 | ||
|
3da1598623 | ||
|
8e33553d38 | ||
|
5718775c5c | ||
|
766b1af9f0 | ||
|
9a59879dc4 | ||
|
abb9ad3e7e | ||
|
2f3117ecba | ||
|
422cdafd82 | ||
|
26f8b5b6d5 | ||
|
e17a45cdea | ||
|
9ddc22ba79 | ||
|
7e080756ef | ||
|
2d9723ab03 | ||
|
e40bdb2209 | ||
|
774aca790a | ||
|
2f8c249259 | ||
|
e0ca450caa | ||
|
12998cfa55 | ||
|
9e2fc60ffa | ||
|
0068f34de1 | ||
|
fc49762f29 | ||
|
0a342f5b10 | ||
|
562eb6ef29 | ||
|
1af7ede9af | ||
|
abf3ed60c6 | ||
|
8536834247 | ||
|
2c1d365a41 | ||
|
51e8cd09a3 | ||
|
7c5fc999f1 | ||
|
fca42969d0 | ||
|
de09e357d7 | ||
|
a605456c36 | ||
|
9c31b39366 | ||
|
5e5742229c | ||
|
ef63cdfb3c | ||
|
bd7b6a093a | ||
|
0bc30a3251 | ||
|
c23b8abe61 | ||
|
a4299e6cbb | ||
|
bcd16e54b6 | ||
|
7f707151a8 | ||
|
6cac211070 | ||
|
494cabcb43 | ||
|
4ef87b991a | ||
|
60ea18e3aa | ||
|
5d061e08e9 | ||
|
78c84ef719 | ||
|
171f1d0075 | ||
|
6d04ee08cc | ||
|
04b90148fe | ||
|
8ec5dd64a7 | ||
|
5da855cea9 | ||
|
d62e2757b8 | ||
|
bdaf8f726e | ||
|
d82fe12f63 | ||
|
6ab8cf8ff0 | ||
|
a380412577 | ||
|
5225ba2bae | ||
|
ce6b5fd305 | ||
|
56b03c50c2 | ||
|
adc86f8888 | ||
|
cb99ee3ded | ||
|
a7e81be28e | ||
|
4a44bc1a56 | ||
|
09f9aa5d08 | ||
|
d0df33e21c | ||
|
b3b79bf497 | ||
|
397edea6d2 | ||
|
53d8d9da5b | ||
|
f1272af2fa | ||
|
3fdb33d7e2 | ||
|
aa78810968 | ||
|
8393da2c0e | ||
|
00f321066e | ||
|
3f7d23916d | ||
|
19e475e91f | ||
|
cba0bfea7d | ||
|
dede5b833b | ||
|
22e24f867a | ||
|
0f3f52c600 | ||
|
d983f04f7d | ||
|
c085b3de4a | ||
|
98a915fbed | ||
|
8e5b78fc23 | ||
|
c690f891e9 | ||
|
b1ae86a7c1 | ||
|
d61a992f7b | ||
|
ef1bdad60e | ||
|
4fe7b7978c | ||
|
a13a5ee33f | ||
|
523a4f3a8f | ||
|
e9e2e06826 | ||
|
92fbd9ff53 | ||
|
ed7d14c7b0 | ||
|
2f3a9e4d2e | ||
|
5272a51cf7 | ||
|
da059510d2 | ||
|
973988b984 | ||
|
7ec4bcd412 | ||
|
3b4d270ab2 | ||
|
aa956ec1e6 | ||
|
28aca3f922 | ||
|
56630531d4 | ||
|
914c4153de | ||
|
372177638d | ||
|
ccbcc8ced3 | ||
|
3f9a07cafb | ||
|
afd5d71236 | ||
|
0b1dfa5cb0 | ||
|
3c651403c4 | ||
|
31d89df75b | ||
|
1d115db049 | ||
|
f36e2b0dbc | ||
|
033bc93a80 | ||
|
ff3e422e2f | ||
|
11e39fb125 | ||
|
0ffca9f1ea | ||
|
b536aa7d3b | ||
|
4e0d016a7c | ||
|
f475c3b094 | ||
|
7ff866db0d | ||
|
e4dc5cf9ec | ||
|
dab254836d | ||
|
2415665979 | ||
|
0815094051 | ||
|
8930d60adc | ||
|
38b8bf6dfe | ||
|
0e9f3a7b1d | ||
|
359d5056d6 | ||
|
ec6f610efb | ||
|
94cbefd702 | ||
|
067a59e575 | ||
|
66bd4621fe | ||
|
157963624c | ||
|
2b44d14143 | ||
|
4a1b3c5e40 | ||
|
c27c07edae | ||
|
dddde22aca | ||
|
febe22724f | ||
|
584dad23df | ||
|
9b427bb9f5 | ||
|
4fb90624f9 | ||
|
9d21124fb0 | ||
|
5e9a8eae09 | ||
|
1cc771b669 | ||
|
df9547306c | ||
|
188da5f314 | ||
|
fa437a506d | ||
|
4e3d13a7e9 | ||
|
1aeaed09e4 | ||
|
ca344ee641 | ||
|
adb1129d7a | ||
|
2f03d98a44 | ||
|
0ff6f29c74 | ||
|
3a6a47fb85 | ||
|
2dff9d9264 | ||
|
1fc9810b71 | ||
|
3e0b831d12 | ||
|
da65528cc2 | ||
|
e0942cff5e | ||
|
0d98b12c3e | ||
|
b393af780e | ||
|
3a783f4d03 | ||
|
b6cc4d8d3e | ||
|
b28f9384ee | ||
|
dd6c47f4e6 | ||
|
49b5a3efe6 | ||
|
440ba88993 | ||
|
1db1a624c3 | ||
|
5aa3198799 | ||
|
c9adc92031 | ||
|
42f7875f26 | ||
|
26aae86af8 | ||
|
e161d1456d | ||
|
96566ebaf4 | ||
|
891743e4ca | ||
|
fd1e7c2b63 | ||
|
bd331a477c | ||
|
8c9747d711 | ||
|
6abe18eb21 | ||
|
ec1cacd253 | ||
|
53d1654387 | ||
|
d48ecbdd9e | ||
|
ab0011c08d | ||
|
df45a32113 | ||
|
51237e2d95 | ||
|
4dac35d898 | ||
|
c4bb3a1e17 | ||
|
f5dd435feb | ||
|
28698b7f59 | ||
|
efead04fd6 | ||
|
fa3b27117f | ||
|
c775580ee2 | ||
|
a913508458 | ||
|
d50eae0645 | ||
|
d2c22d6faa | ||
|
2d74c84d30 | ||
|
7602c0751f | ||
|
0250dbac5a | ||
|
871b569ca2 | ||
|
87b0e14080 | ||
|
8cfa54f721 | ||
|
c6a492330e | ||
|
e180e1fc74 | ||
|
01e1431472 | ||
|
5e1c44db44 | ||
|
2da146f2ec | ||
|
166c614769 | ||
|
06e0296e4c | ||
|
56c4489db6 | ||
|
e5b8a23f4c | ||
|
5e897a3b65 | ||
|
c71c94d10d | ||
|
ef75e9cb72 | ||
|
16b7906df2 | ||
|
e76f5196be | ||
|
eea804cf08 | ||
|
d9537a2107 | ||
|
a5da07d5a0 | ||
|
57baa173ec | ||
|
2d662fd2ed | ||
|
c3b2a996c8 | ||
|
8441880e39 | ||
|
055400f30e | ||
|
45475d534c | ||
|
d542306372 | ||
|
93b339fe09 | ||
|
5f2ad38d0c | ||
|
5617d60de3 | ||
|
b770ef158e | ||
|
285842db7c | ||
|
e10520730b | ||
|
a7aa5c272a | ||
|
26f645856e | ||
|
08bea1469d | ||
|
0cb79567d4 | ||
|
29c2bf39de | ||
|
79bfa2b25d | ||
|
58388d857b | ||
|
c79621c850 | ||
|
56e077d2e5 | ||
|
c1efc71f54 | ||
|
efb3bffc4c | ||
|
d65ccbf74a | ||
|
bf88193d24 | ||
|
87f2f858a0 | ||
|
4dd146b6b1 | ||
|
1b4229159f |
6
.babelrc
Normal file
6
.babelrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"presets": ["@babel/env"],
|
||||
"plugins": [
|
||||
"@babel/plugin-syntax-class-properties"
|
||||
]
|
||||
}
|
16
.eslintrc.yml
Normal file
16
.eslintrc.yml
Normal file
@ -0,0 +1,16 @@
|
||||
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"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
|
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@ -16,42 +16,46 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: michaelwoods/onstar2mqtt
|
||||
tag-sha: true
|
||||
tag-schedule: weekly
|
||||
flavor: |
|
||||
latest=true
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=schedule,pattern=weekly
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.1.3
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm run lint
|
||||
- run: npm test
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Push to DockerHub
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
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
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# 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
|
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@ -9,28 +9,32 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: michaelwoods/onstar2mqtt
|
||||
tag-sha: true
|
||||
flavor: |
|
||||
latest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Push to DockerHub
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
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,3 +2,4 @@
|
||||
.nyc_output/
|
||||
node_modules/
|
||||
|
||||
onstar2mqtt.env
|
||||
|
@ -1,10 +1,11 @@
|
||||
FROM node:12
|
||||
FROM node:18-alpine
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY ["package.json", "/app/"]
|
||||
RUN npm install
|
||||
COPY ["package-lock.json", "/app/"]
|
||||
RUN npm ci --omit=dev --no-fund
|
||||
|
||||
COPY ["src", "/app/src"]
|
||||
|
||||
|
192
HA-MQTT.md
192
HA-MQTT.md
@ -1,11 +1,100 @@
|
||||
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
|
||||
Create a new dashboard, or use the cards in your own view. The `mdi:car-electric` icon works well here.
|
||||
|
||||

|
||||
|
||||
yaml:
|
||||
#### dashboard yaml:
|
||||
```yaml
|
||||
views:
|
||||
- badges: []
|
||||
@ -44,22 +133,54 @@ views:
|
||||
icon: 'mdi:car-tire-alert'
|
||||
columns: 2
|
||||
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
|
||||
title: Mileage
|
||||
entities:
|
||||
- entity: sensor.odometer
|
||||
- entity: sensor.lifetime_energy_used
|
||||
- entity: sensor.lifetime_mpge
|
||||
- entity: sensor.lifetime_efficiency
|
||||
- entity: sensor.electric_economy
|
||||
- type: glance
|
||||
state_color: true
|
||||
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:
|
||||
- entity: sensor.ambient_air_temperature
|
||||
name: Ambient
|
||||
@ -67,20 +188,39 @@ views:
|
||||
name: Battery
|
||||
- entity: sensor.kewr_daynight_temperature
|
||||
name: Outdoor
|
||||
title: Temperature
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: binary_sensor.ev_plug_state
|
||||
- entity: binary_sensor.ev_charge_state
|
||||
- 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
|
||||
name: Temperature
|
||||
hours_to_show: 24
|
||||
points_per_hour: 1
|
||||
line_width: 2
|
||||
- type: grid
|
||||
cards:
|
||||
- type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
entity: script.car_start_vehicle
|
||||
name: Start
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
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.
|
19
README.md
19
README.md
@ -1,7 +1,9 @@
|
||||
# onstar2mqtt
|
||||
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.
|
||||
A service that utilizes the [OnStarJS](https://github.com/samrum/OnStarJS) library to expose OnStar data to MQTT topics.
|
||||
|
||||
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.
|
||||
The functionality is mostly focused around EVs (specifically the Bolt EV), however PRs for other vehicle types are certainly welcome.
|
||||
|
||||
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
|
||||
Collect the following information:
|
||||
@ -11,7 +13,7 @@ Collect the following information:
|
||||
1. MQTT server information: hostname, username, password
|
||||
1. If using TLS, define `MQTT_PORT` and `MQTT_TLS=true`
|
||||
|
||||
Supply these values to the ENV vars below.
|
||||
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.
|
||||
### [Docker](https://hub.docker.com/r/michaelwoods/onstar2mqtt)
|
||||
|
||||
```shell
|
||||
@ -49,10 +51,10 @@ MQTT_PASSWORD=
|
||||
```
|
||||
### Node.js
|
||||
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, only tested with Node.js 12.x.
|
||||
`npm run start`. Currently, this is only tested with Node.js 18.x.
|
||||
|
||||
### Home Assistant configuration templates
|
||||
MQTT auto discovery is enabled. For further integrations see [HA-MQTT.md](HA-MQTT.md).
|
||||
MQTT auto discovery is enabled. For further integrations and screenshots see [HA-MQTT.md](HA-MQTT.md).
|
||||
|
||||
## Development
|
||||
### Running
|
||||
@ -60,13 +62,8 @@ MQTT auto discovery is enabled. For further integrations see [HA-MQTT.md](HA-MQT
|
||||
### Testing
|
||||
`npm run test`
|
||||
### Coverage
|
||||
`rpm run 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).
|
||||
|
||||
## TODO
|
||||
1. Logging library
|
||||
1. Figure out metric->imperial unit handling
|
||||
1. Enable write actions to lock doors, flash lights, remote start, etc.
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 166 KiB |
10383
package-lock.json
generated
10383
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "onstar2mqtt",
|
||||
"version": "1.0.2",
|
||||
"version": "1.5.4",
|
||||
"description": "OnStarJS wrapper for MQTT",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"coverage": "nyc npm test",
|
||||
"lint": "npx eslint src test",
|
||||
"start": "node src/index.js",
|
||||
"test": "mocha"
|
||||
},
|
||||
@ -16,7 +17,10 @@
|
||||
"onstar",
|
||||
"mqtt",
|
||||
"gm",
|
||||
"chevrolet"
|
||||
"chevrolet",
|
||||
"homeassistant",
|
||||
"home-assistant",
|
||||
"home assistant"
|
||||
],
|
||||
"author": "Michael Woods",
|
||||
"license": "MIT",
|
||||
@ -25,14 +29,23 @@
|
||||
},
|
||||
"homepage": "https://github.com/michaelwoods/onstar2mqtt#readme",
|
||||
"dependencies": {
|
||||
"async-mqtt": "^2.6.1",
|
||||
"async-mqtt": "^2.6.3",
|
||||
"convert-units": "^2.3.4",
|
||||
"lodash": "^4.17.20",
|
||||
"onstarjs": "^2.0.10",
|
||||
"uuid": "^8.3.2"
|
||||
"lodash": "^4.17.21",
|
||||
"onstarjs": "^2.3.16",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^8.2.1",
|
||||
"@babel/eslint-parser": "^7.19.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"
|
||||
}
|
||||
}
|
||||
|
126
src/commands.js
Normal file
126
src/commands.js
Normal file
@ -0,0 +1,126 @@
|
||||
|
||||
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,6 +10,9 @@ class Diagnostic {
|
||||
d => _.has(d, 'value') && _.has(d, 'unit')
|
||||
);
|
||||
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() {
|
||||
@ -24,6 +27,29 @@ class Diagnostic {
|
||||
}
|
||||
|
||||
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) {
|
||||
this._name = ele.name;
|
||||
this.measurement = new Measurement(ele.value, ele.unit);
|
||||
@ -41,6 +67,10 @@ class DiagnosticElement {
|
||||
return this.measurement.unit;
|
||||
}
|
||||
|
||||
get isConvertible() {
|
||||
return this.measurement.isConvertible;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.name}: ${this.measurement.toString()}`;
|
||||
}
|
||||
|
206
src/index.js
206
src/index.js
@ -5,6 +5,9 @@ const _ = require('lodash');
|
||||
const Vehicle = require('./vehicle');
|
||||
const {Diagnostic} = require('./diagnostic');
|
||||
const MQTT = require('./mqtt');
|
||||
const Commands = require('./commands');
|
||||
const logger = require('./logger');
|
||||
|
||||
|
||||
const onstarConfig = {
|
||||
deviceId: process.env.ONSTAR_DEVICEID || uuidv4(),
|
||||
@ -12,9 +15,13 @@ const onstarConfig = {
|
||||
username: process.env.ONSTAR_USERNAME,
|
||||
password: process.env.ONSTAR_PASSWORD,
|
||||
onStarPin: process.env.ONSTAR_PIN,
|
||||
checkRequestStatus: process.env.ONSTAR_SYNC === "true" || true,
|
||||
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000) // 30 min
|
||||
checkRequestStatus: _.get(process.env, 'ONSTAR_SYNC', 'true') === 'true',
|
||||
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 = {
|
||||
host: process.env.MQTT_HOST || 'localhost',
|
||||
@ -23,66 +30,165 @@ const mqttConfig = {
|
||||
port: parseInt(process.env.MQTT_PORT) || 1883,
|
||||
tls: process.env.MQTT_TLS || false,
|
||||
prefix: process.env.MQTT_PREFIX || 'homeassistant',
|
||||
namePrefix: process.env.MQTT_NAME_PREFIX || '',
|
||||
};
|
||||
logger.info('MQTT Config', {mqttConfig});
|
||||
|
||||
const init = () => new Commands(OnStar.create(onstarConfig));
|
||||
|
||||
const getVehicles = async commands => {
|
||||
logger.info('Requesting vehicles');
|
||||
const vehiclesRes = await commands.getAccountVehicles();
|
||||
logger.info('Vehicle request status', {status: _.get(vehiclesRes, 'status')});
|
||||
const vehicles = _.map(
|
||||
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
|
||||
v => new Vehicle(v)
|
||||
);
|
||||
logger.debug('Vehicle request response', {vehicles: _.map(vehicles, v => v.toString())});
|
||||
return vehicles;
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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 commands = init();
|
||||
const vehicle = await getCurrentVehicle(commands);
|
||||
|
||||
console.log('Requesting vehicles.');
|
||||
const vehiclesRes = await onStar.getAccountVehicles();
|
||||
console.log(_.get(vehiclesRes, 'status'));
|
||||
const vehicles = _.map(
|
||||
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
|
||||
v => new Vehicle(v)
|
||||
);
|
||||
console.log('Vehicles returned:');
|
||||
for (const v of vehicles) {
|
||||
console.log(v.toString());
|
||||
}
|
||||
const mqttHA = new MQTT('homeassistant', vehicles[0].vin);
|
||||
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 () => {
|
||||
// Note: the library is set to use only the configured VIN, but using multiple for future proofing.
|
||||
for (const v of vehicles) {
|
||||
console.log('Requesting diagnostics:')
|
||||
const statsRes = await onStar.diagnostics({
|
||||
diagnosticItem: v.getSupported()
|
||||
});
|
||||
console.log(_.get(statsRes, 'status'));
|
||||
const stats = _.map(
|
||||
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
|
||||
d => new Diagnostic(d)
|
||||
);
|
||||
const states = new Map();
|
||||
const v = vehicle;
|
||||
logger.info('Requesting diagnostics');
|
||||
const statsRes = await commands.diagnostics({diagnosticItem: v.getSupported()});
|
||||
logger.info('Diagnostic request status', {status: _.get(statsRes, 'status')});
|
||||
const stats = _.map(
|
||||
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
|
||||
d => new Diagnostic(d)
|
||||
);
|
||||
logger.debug('Diagnostic request response', {stats: _.map(stats, s => s.toString())});
|
||||
|
||||
for (const s of stats) {
|
||||
if (!s.hasElements()) {
|
||||
continue;
|
||||
}
|
||||
// configure, then set state
|
||||
for (const d of s.diagnosticElements) {
|
||||
console.log(mqttHA.getConfigTopic(d));
|
||||
console.log(JSON.stringify(mqttHA.getConfigPayload(s, d)));
|
||||
await client.publish(mqttHA.getConfigTopic(d), JSON.stringify(mqttHA.getConfigPayload(s, d)));
|
||||
}
|
||||
console.log(mqttHA.getStateTopic(s));
|
||||
console.log(JSON.stringify(mqttHA.getStatePayload(s)));
|
||||
await client.publish(mqttHA.getStateTopic(s), JSON.stringify(mqttHA.getStatePayload(s)));
|
||||
for (const s of stats) {
|
||||
if (!s.hasElements()) {
|
||||
continue;
|
||||
}
|
||||
// configure once, then set or update states
|
||||
for (const d of s.diagnosticElements) {
|
||||
const topic = mqttHA.getConfigTopic(d)
|
||||
const payload = mqttHA.getConfigPayload(s, d);
|
||||
configurations.set(topic, {configured: false, payload});
|
||||
}
|
||||
|
||||
const topic = mqttHA.getStateTopic(s);
|
||||
const payload = 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 = () => run()
|
||||
.then(() => console.log('Done, sleeping.'))
|
||||
.catch(e => console.error(e))
|
||||
const main = async () => run()
|
||||
.then(() => logger.info('Updates complete, sleeping.'))
|
||||
.catch(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});
|
||||
}
|
||||
});
|
||||
|
||||
main();
|
||||
loop = setInterval(main, onstarConfig.refreshInterval);
|
||||
await main();
|
||||
setInterval(main, onstarConfig.refreshInterval);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger.error('Main function error.', {error: e});
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
12
src/logger.js
Normal file
12
src/logger.js
Normal file
@ -0,0 +1,12 @@
|
||||
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,9 +1,20 @@
|
||||
// const convert = require('convert-units');
|
||||
const _ = require('lodash');
|
||||
const convert = require('convert-units');
|
||||
|
||||
class Measurement {
|
||||
static CONVERTABLE_UNITS = [
|
||||
'°C',
|
||||
'km',
|
||||
'kPa',
|
||||
'km/l(e)',
|
||||
// Helps with conversion to Gallons.
|
||||
'lit'
|
||||
];
|
||||
|
||||
constructor(value, unit) {
|
||||
this.value = value;
|
||||
this.unit = Measurement.correctUnitName(unit);
|
||||
this.isConvertible = _.includes(Measurement.CONVERTABLE_UNITS, this.unit);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,10 +33,12 @@ class Measurement {
|
||||
case 'KPa':
|
||||
return 'kPa';
|
||||
case 'kmple':
|
||||
return 'km/l(e)'; // TODO check on this
|
||||
return 'km/l(e)';
|
||||
case 'volts':
|
||||
case 'Volts':
|
||||
return 'V';
|
||||
case 'l':
|
||||
return 'lit';
|
||||
// these are states
|
||||
case 'Stat':
|
||||
case 'N/A':
|
||||
@ -36,20 +49,59 @@ class Measurement {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this may not be required. Check consuming application.
|
||||
/*static convertToImperial(value, unit) {
|
||||
switch(unit) {
|
||||
case 'Cel':
|
||||
const val = convert(value).from('C').to('F');
|
||||
return new Measurement(val, 'F');
|
||||
default:
|
||||
return new Measurement(value, unit);
|
||||
/**
|
||||
*
|
||||
* @param {string|number} value
|
||||
* @param {string} unit
|
||||
* @returns {string|number}
|
||||
*/
|
||||
static convertValue(value, unit) {
|
||||
switch (unit) {
|
||||
case '°C':
|
||||
value = _.round(convert(value).from('C').to('F'));
|
||||
break;
|
||||
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:
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.value}${this.unit}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Measurement;
|
||||
module.exports = Measurement;
|
||||
|
97
src/mqtt.js
97
src/mqtt.js
@ -36,9 +36,11 @@ const _ = require('lodash');
|
||||
* }
|
||||
*/
|
||||
class MQTT {
|
||||
constructor(prefix = 'homeassistant', instance = 'XXX') {
|
||||
constructor(vehicle, prefix = 'homeassistant', namePrefix) {
|
||||
this.prefix = prefix;
|
||||
this.instance = instance;
|
||||
this.vehicle = vehicle;
|
||||
this.instance = vehicle.vin;
|
||||
this.namePrefix = namePrefix
|
||||
}
|
||||
|
||||
static convertName(name) {
|
||||
@ -56,19 +58,38 @@ class MQTT {
|
||||
case 'PRIORITY CHARGE INDICATOR':
|
||||
case 'PRIORITY CHARGE STATUS':
|
||||
return 'binary_sensor';
|
||||
case 'getLocation':
|
||||
return 'device_tracker';
|
||||
default:
|
||||
return 'sensor';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'sensor'|'binary_sensor'} type
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
addNamePrefix(name) {
|
||||
if (!this.namePrefix) return name
|
||||
return `${this.namePrefix} ${name}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'sensor'|'binary_sensor'|'device_tracker'} type
|
||||
* @returns {string}
|
||||
*/
|
||||
getBaseTopic(type = 'sensor') {
|
||||
return `${this.prefix}/${type}/${this.instance}`;
|
||||
}
|
||||
|
||||
getAvailabilityTopic() {
|
||||
return `${this.prefix}/${this.instance}/available`;
|
||||
}
|
||||
|
||||
getCommandTopic() {
|
||||
return `${this.prefix}/${this.instance}/command`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DiagnosticElement} diag
|
||||
@ -120,6 +141,7 @@ class MQTT {
|
||||
break;
|
||||
default:
|
||||
// coerce to number if possible, API uses strings :eyeroll:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const num = _.toNumber(e.value);
|
||||
value = _.isNaN(num) ? e.value : num;
|
||||
break;
|
||||
@ -129,19 +151,46 @@ class MQTT {
|
||||
return state;
|
||||
}
|
||||
|
||||
mapConfigPayload(diag, diagEl, device_class, name, attr) {
|
||||
mapBaseConfigPayload(diag, diagEl, device_class, name, attr) {
|
||||
name = name || MQTT.convertFriendlyName(diagEl.name);
|
||||
// TODO availability
|
||||
name = this.addNamePrefix(name);
|
||||
// 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 {
|
||||
device_class,
|
||||
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),
|
||||
unit_of_measurement: diagEl.unit,
|
||||
value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`,
|
||||
json_attributes_template: attr
|
||||
json_attributes_topic: _.isUndefined(attr) ? undefined : this.getStateTopic(diag),
|
||||
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
|
||||
@ -153,31 +202,43 @@ class MQTT {
|
||||
case 'LIFETIME ENERGY USED':
|
||||
case 'LIFETIME EFFICIENCY':
|
||||
case 'ELECTRIC ECONOMY':
|
||||
return this.mapConfigPayload(diag, diagEl, 'energy');
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'energy');
|
||||
case 'INTERM VOLT BATT VOLT':
|
||||
case 'EV PLUG VOLTAGE':
|
||||
return this.mapConfigPayload(diag, diagEl, 'voltage');
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'voltage');
|
||||
case 'HYBRID BATTERY MINIMUM TEMPERATURE':
|
||||
case 'AMBIENT AIR TEMPERATURE':
|
||||
return this.mapConfigPayload(diag, diagEl, 'temperature');
|
||||
case 'AMBIENT AIR TEMPERATURE F':
|
||||
case 'ENGINE COOLANT TEMP':
|
||||
case 'ENGINE COOLANT TEMP F':
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'temperature');
|
||||
case 'EV BATTERY LEVEL':
|
||||
return this.mapConfigPayload(diag, diagEl, 'battery');
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'battery');
|
||||
case 'TIRE PRESSURE LF':
|
||||
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', `{{ {'recommendation': value_json.${MQTT.convertName('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':
|
||||
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | 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':
|
||||
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | 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':
|
||||
return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {'recommendation': value_json.${MQTT.convertName('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
|
||||
case 'EV PLUG STATE': // unplugged/plugged
|
||||
return this.mapConfigPayload(diag, diagEl, 'plug');
|
||||
return this.mapBinarySensorConfigPayload(diag, diagEl, 'plug');
|
||||
case 'EV CHARGE STATE': // not_charging/charging
|
||||
return this.mapConfigPayload(diag, diagEl, 'battery_charging');
|
||||
return this.mapBinarySensorConfigPayload(diag, diagEl, 'battery_charging');
|
||||
// binary_sensor, but no applicable device_class
|
||||
case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE
|
||||
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
|
||||
return this.mapBinarySensorConfigPayload(diag, diagEl);
|
||||
// no device class, camel case name
|
||||
case 'EV RANGE':
|
||||
case 'ODOMETER':
|
||||
@ -186,7 +247,7 @@ class MQTT {
|
||||
case 'LIFETIME MPGE':
|
||||
case 'CHARGER POWER LEVEL':
|
||||
default:
|
||||
return this.mapConfigPayload(diag, diagEl);
|
||||
return this.mapSensorConfigPayload(diag, diagEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class Vehicle {
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.year} ${this.make} ${this.model} ${this.vin}`;
|
||||
return `${this.year} ${this.make} ${this.model}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
const assert = require('assert');
|
||||
const _ = require('lodash');
|
||||
|
||||
const { Diagnostic } = require('../src/diagnostic');
|
||||
const { Diagnostic, DiagnosticElement } = require('../src/diagnostic');
|
||||
const apiResponse = require('./diagnostic.sample.json');
|
||||
|
||||
describe('Diagnostics', () => {
|
||||
@ -12,13 +12,13 @@ describe('Diagnostics', () => {
|
||||
|
||||
it('should parse a diagnostic response', () => {
|
||||
assert.strictEqual(d.name, 'AMBIENT AIR TEMPERATURE');
|
||||
assert.strictEqual(d.diagnosticElements.length, 1);
|
||||
assert.strictEqual(d.diagnosticElements.length, 2);
|
||||
});
|
||||
|
||||
it('should toString() correctly', () => {
|
||||
const output = d.toString().trimEnd();
|
||||
const lines = output.split(/\r\n|\r|\n/);
|
||||
assert.strictEqual(lines.length, 2);
|
||||
assert.strictEqual(lines.length, 3);
|
||||
assert.strictEqual(lines[0], 'AMBIENT AIR TEMPERATURE:');
|
||||
});
|
||||
});
|
||||
@ -28,15 +28,19 @@ describe('Diagnostics', () => {
|
||||
it('should parse a diagnostic element', () => {
|
||||
assert.strictEqual(d.name, 'TIRE PRESSURE');
|
||||
assert.ok(_.isArray(d.diagnosticElements));
|
||||
assert.strictEqual(d.diagnosticElements.length, 6);
|
||||
assert.strictEqual(d.diagnosticElements.length, 12);
|
||||
});
|
||||
|
||||
it('should toString() correctly', () => {
|
||||
const output = d.toString().trimEnd();
|
||||
const lines = output.split(/\r\n|\r|\n/);
|
||||
assert.strictEqual(lines.length, 7);
|
||||
assert.strictEqual(lines.length, 13);
|
||||
assert.strictEqual(lines[0], 'TIRE PRESSURE:');
|
||||
assert.strictEqual(lines[1], ' TIRE PRESSURE LF: 240.0kPa');
|
||||
});
|
||||
|
||||
it('should strip non-alpha chars', () => {
|
||||
assert.strictEqual(DiagnosticElement.convertName('TEMP', '°F'), 'TEMP F');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,11 +3,13 @@ const _ = require('lodash');
|
||||
|
||||
const { Diagnostic } = require('../src/diagnostic');
|
||||
const MQTT = require('../src/mqtt');
|
||||
const Vehicle = require('../src/vehicle');
|
||||
const apiResponse = require('./diagnostic.sample.json');
|
||||
|
||||
describe('MQTT', () => {
|
||||
let mqtt;
|
||||
beforeEach(() => mqtt = new MQTT());
|
||||
let vehicle = new Vehicle({make: 'foo', model: 'bar', vin: 'XXX', year: 2020});
|
||||
beforeEach(() => mqtt = new MQTT(vehicle));
|
||||
|
||||
it('should set defaults', () => {
|
||||
assert.strictEqual(mqtt.prefix, 'homeassistant');
|
||||
@ -26,8 +28,27 @@ describe('MQTT', () => {
|
||||
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', () => {
|
||||
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', () => {
|
||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
|
||||
|
||||
@ -35,7 +56,7 @@ describe('MQTT', () => {
|
||||
assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config');
|
||||
});
|
||||
it('should generate state topics', () => {
|
||||
assert.strictEqual(mqtt.getStateTopic(d, d.diagnosticElements[0]), 'homeassistant/sensor/XXX/ambient_air_temperature/state');
|
||||
assert.strictEqual(mqtt.getStateTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/state');
|
||||
});
|
||||
});
|
||||
|
||||
@ -56,17 +77,52 @@ describe('MQTT', () => {
|
||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
|
||||
it('should generate config payloads', () => {
|
||||
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',
|
||||
json_attributes_template: undefined,
|
||||
name: 'Ambient Air Temperature',
|
||||
payload_available: 'true',
|
||||
payload_not_available: 'false',
|
||||
state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state',
|
||||
unique_id: 'xxx-ambient-air-temperature',
|
||||
json_attributes_topic: undefined,
|
||||
unit_of_measurement: '°C',
|
||||
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', () => {
|
||||
assert.deepStrictEqual(mqtt.getStatePayload(d), {
|
||||
ambient_air_temperature: 15
|
||||
ambient_air_temperature: 15,
|
||||
ambient_air_temperature_f: 59
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -75,19 +131,33 @@ describe('MQTT', () => {
|
||||
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]')));
|
||||
it('should generate config payloads', () => {
|
||||
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), {
|
||||
device_class: undefined,
|
||||
json_attributes_template: undefined,
|
||||
name: 'Priority Charge Indicator',
|
||||
state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state',
|
||||
unit_of_measurement: undefined,
|
||||
value_template: '{{ value_json.priority_charge_indicator }}'
|
||||
availability_topic: 'homeassistant/XXX/available',
|
||||
device: {
|
||||
identifiers: [
|
||||
'XXX'
|
||||
],
|
||||
manufacturer: 'foo',
|
||||
model: 2020,
|
||||
name: '2020 foo bar'
|
||||
},
|
||||
device_class: undefined,
|
||||
json_attributes_template: undefined,
|
||||
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',
|
||||
unique_id: 'xxx-priority-charge-indicator',
|
||||
json_attributes_topic: undefined,
|
||||
value_template: '{{ value_json.priority_charge_indicator }}'
|
||||
});
|
||||
});
|
||||
it('should generate state payloads', () => {
|
||||
assert.deepStrictEqual(mqtt.getStatePayload(d), {
|
||||
ev_charge_state: false,
|
||||
priority_charge_indicator: false,
|
||||
priority_charge_status: false
|
||||
ev_charge_state: false,
|
||||
priority_charge_indicator: false,
|
||||
priority_charge_status: false
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -96,10 +166,23 @@ describe('MQTT', () => {
|
||||
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 }}',
|
||||
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', () => {
|
||||
assert.strictEqual(v.toString(), '2020 Chevrolet Bolt EV foobarVIN')
|
||||
assert.strictEqual(v.toString(), '2020 Chevrolet Bolt EV')
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user