Compare commits
290 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 |
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.3.4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v3.3.0
|
||||
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.5
|
||||
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.2.0
|
||||
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.9.0
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Push to DockerHub
|
||||
uses: docker/build-push-action@v2.5.0
|
||||
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.3.4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: crazy-max/ghaction-docker-meta@v3.3.0
|
||||
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.2.0
|
||||
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.9.0
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Push to DockerHub
|
||||
uses: docker/build-push-action@v2.5.0
|
||||
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,11 +1,11 @@
|
||||
FROM node:12
|
||||
FROM node:18-alpine
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY ["package.json", "/app/"]
|
||||
COPY ["package-lock.json", "/app/"]
|
||||
RUN npm install --no-fund
|
||||
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.
|
@ -13,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
|
||||
@ -51,7 +51,7 @@ 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, this is 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 and screenshots see [HA-MQTT.md](HA-MQTT.md).
|
||||
@ -67,7 +67,3 @@ MQTT auto discovery is enabled. For further integrations and screenshots see [HA
|
||||
`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
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 166 KiB |
10146
package-lock.json
generated
10146
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "onstar2mqtt",
|
||||
"version": "1.1.0",
|
||||
"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"
|
||||
},
|
||||
@ -28,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.21",
|
||||
"onstarjs": "^2.2.1",
|
||||
"uuid": "^8.3.2"
|
||||
"onstarjs": "^2.3.16",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.0.0",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,10 @@ class Commands {
|
||||
return this.onstar.setChargingProfile();
|
||||
}
|
||||
|
||||
async getLocation() {
|
||||
return this.onstar.location();
|
||||
}
|
||||
|
||||
async diagnostics({diagnosticItem = [
|
||||
Commands.CONSTANTS.DIAGNOSTICS.ODOMETER,
|
||||
Commands.CONSTANTS.DIAGNOSTICS.TIRE_PRESSURE,
|
||||
|
@ -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()}`;
|
||||
}
|
||||
|
169
src/index.js
169
src/index.js
@ -1,4 +1,3 @@
|
||||
|
||||
const OnStar = require('onstarjs');
|
||||
const mqtt = require('async-mqtt');
|
||||
const uuidv4 = require('uuid').v4;
|
||||
@ -7,6 +6,8 @@ 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(),
|
||||
@ -14,10 +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,
|
||||
checkRequestStatus: _.get(process.env, 'ONSTAR_SYNC', 'true') === 'true',
|
||||
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000), // 30 min
|
||||
allowCommands: _.toLower(_.get(process, 'env.ONSTAR_ALLOW_COMMANDS', 'true')) === 'true'
|
||||
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',
|
||||
@ -26,72 +30,109 @@ 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});
|
||||
|
||||
let loop, commands, vehicles;
|
||||
const init = () => new Commands(OnStar.create(onstarConfig));
|
||||
|
||||
const init = async () => {
|
||||
commands = new Commands(OnStar.create(onstarConfig));
|
||||
console.log('Requesting vehicles.');
|
||||
const getVehicles = async commands => {
|
||||
logger.info('Requesting vehicles');
|
||||
const vehiclesRes = await commands.getAccountVehicles();
|
||||
console.log(_.get(vehiclesRes, 'status'));
|
||||
vehicles = _.map(
|
||||
logger.info('Vehicle request status', {status: _.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());
|
||||
}
|
||||
logger.debug('Vehicle request response', {vehicles: _.map(vehicles, v => v.toString())});
|
||||
return vehicles;
|
||||
}
|
||||
|
||||
const connectMQTT = async () => {
|
||||
const mqttHA = new MQTT(vehicles[0], 'homeassistant');
|
||||
const availTopic = mqttHA.getAvailabilityTopic();
|
||||
const client = await mqtt.connectAsync(`${mqttConfig.tls
|
||||
? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`, {
|
||||
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: availTopic, payload: 'false', retain: true}
|
||||
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}));
|
||||
});
|
||||
|
||||
if (onstarConfig.allowCommands) {
|
||||
client.on('message', (topic, message) => {
|
||||
console.log(`Subscription message: ${topic} ${message}`);
|
||||
const {command, options} = JSON.parse(message);
|
||||
const commandFn = commands[command].bind(commands);
|
||||
commandFn(options || {})
|
||||
.then(() => console.log(`Command completed: ${command}`))
|
||||
.catch(err=> console.error(`Command error: ${command} ${err}`));
|
||||
});
|
||||
const topic = mqttHA.getCommandTopic();
|
||||
console.log(`Subscribed to: ${topic}`);
|
||||
await client.subscribe(topic);
|
||||
}
|
||||
|
||||
await client.publish(availTopic, 'true', {retain: true});
|
||||
return {mqttHA, client};
|
||||
const topic = mqttHA.getCommandTopic();
|
||||
logger.info('Subscribed to command topic', {topic});
|
||||
await client.subscribe(topic);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
const commands = init();
|
||||
const vehicle = await getCurrentVehicle(commands);
|
||||
|
||||
const {mqttHA, client} = await connectMQTT();
|
||||
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 states = new Map();
|
||||
const v = vehicles[0];
|
||||
console.log('Requesting diagnostics:')
|
||||
const statsRes = await commands.diagnostics({
|
||||
diagnosticItem: v.getSupported()
|
||||
});
|
||||
console.log(_.get(statsRes, 'status'));
|
||||
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()) {
|
||||
@ -108,30 +149,46 @@ const connectMQTT = async () => {
|
||||
const payload = mqttHA.getStatePayload(s);
|
||||
states.set(topic, payload);
|
||||
}
|
||||
// publish configs
|
||||
const publishes = [];
|
||||
// publish sensor configs
|
||||
for (let [topic, config] of configurations) {
|
||||
// configure once
|
||||
if (!config.configured) {
|
||||
config.configured = true;
|
||||
const {payload} = config;
|
||||
console.log(`${topic} ${JSON.stringify(payload)}`);
|
||||
await client.publish(topic, JSON.stringify(payload), {retain: true});
|
||||
logger.info('Publishing message', {topic, payload});
|
||||
publishes.push(
|
||||
client.publish(topic, JSON.stringify(payload), {retain: true})
|
||||
);
|
||||
}
|
||||
}
|
||||
// update states
|
||||
// update sensor states
|
||||
for (let [topic, state] of states) {
|
||||
console.log(`${topic} ${JSON.stringify(state)}`);
|
||||
await client.publish(topic, JSON.stringify(state), {retain: true});
|
||||
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});
|
||||
}
|
||||
});
|
||||
|
||||
await main();
|
||||
loop = setInterval(main, onstarConfig.refreshInterval);
|
||||
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;
|
||||
|
35
src/mqtt.js
35
src/mqtt.js
@ -36,10 +36,11 @@ const _ = require('lodash');
|
||||
* }
|
||||
*/
|
||||
class MQTT {
|
||||
constructor(vehicle, prefix = 'homeassistant') {
|
||||
constructor(vehicle, prefix = 'homeassistant', namePrefix) {
|
||||
this.prefix = prefix;
|
||||
this.vehicle = vehicle;
|
||||
this.instance = vehicle.vin;
|
||||
this.namePrefix = namePrefix
|
||||
}
|
||||
|
||||
static convertName(name) {
|
||||
@ -57,13 +58,24 @@ 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') {
|
||||
@ -129,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;
|
||||
@ -140,6 +153,10 @@ class MQTT {
|
||||
|
||||
mapBaseConfigPayload(diag, diagEl, device_class, name, attr) {
|
||||
name = name || MQTT.convertFriendlyName(diagEl.name);
|
||||
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,
|
||||
@ -155,7 +172,8 @@ class MQTT {
|
||||
state_topic: this.getStateTopic(diag),
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@ -190,17 +208,28 @@ class MQTT {
|
||||
return this.mapSensorConfigPayload(diag, diagEl, 'voltage');
|
||||
case 'HYBRID BATTERY MINIMUM TEMPERATURE':
|
||||
case 'AMBIENT AIR 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.mapSensorConfigPayload(diag, diagEl, 'battery');
|
||||
case 'TIRE PRESSURE LF':
|
||||
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.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.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.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.mapBinarySensorConfigPayload(diag, diagEl, 'plug');
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -28,6 +28,16 @@ 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;
|
||||
|
||||
@ -82,14 +92,37 @@ describe('MQTT', () => {
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -115,6 +148,7 @@ describe('MQTT', () => {
|
||||
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 }}'
|
||||
});
|
||||
@ -147,6 +181,7 @@ describe('MQTT', () => {
|
||||
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 }}'
|
||||
|
Loading…
x
Reference in New Issue
Block a user