Skip to content

Commit

Permalink
Initial Version
Browse files Browse the repository at this point in the history
  • Loading branch information
C64Axel committed Jul 21, 2024
1 parent 4c320db commit 7ec5e04
Show file tree
Hide file tree
Showing 23 changed files with 1,955 additions and 1 deletion.
69 changes: 69 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Docker

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

on:
push:
branches: [ "main" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]

env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}


jobs:
build:

runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.11-slim

# Working directory for the application
WORKDIR /usr/src/app

# Set Entrypoint with hard-coded options
ENTRYPOINT ["python3", "./mz2mqtt.py"]

COPY requirements.txt /usr/src/app/

RUN apt update && apt install -y build-essential \
&& pip3 install --no-cache-dir -r requirements.txt \
&& apt purge -y --auto-remove build-essential && apt clean

# Copy everything to the working directory (Python files, templates, config) in one go.
COPY . /usr/src/app/
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,65 @@
# mz2mqtt
Send Car Data to mqtt
**Publish all Car Data to MQTT**

![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)
![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)
---
># !!! WARNING !!! !!! WARNING !!!
>***A too frequent refresh of the data can discharge your 12V starter battery of the car.
So use this Program at your own risk***
---

This Program based on code from bdr99, and it may stop working at any time without warning.

---

Prerequisites:
1. Set up your Car in the app.
2. Create a second Driver for mazda2mqtt.

## Installation Guide:
Clone the git repository.
Create a virtual environment and install the requirements:
```
apt install python-virtualenv
cd mz2mqtt
virtualenv -p python3 ../mz2mqtt.env
source ../mz2mqtt.env/bin/activate
pip3 install -r requirements.txt
```
Then copy config_example.yaml to config.yaml an insert your data.
Start mz2mqtt:
```
cd mz2mqtt
source ../mz2mqtt.env/bin/activate
python mz2mqtt.py
```

Or download the Docker Image
```
docker pull ghcr.io/c64axel/mz2mqtt:main
```
Start the container with /usr/src/app/config.yaml mapped to the config file
```
docker run -d --name mz2mqtt --restart unless-stopped -v <YOUR_DIR/config.yaml>:/usr/src/app/config.yaml mz2mqtt:main
```
---
**MQTT-API**

To trigger a manual refresh for one car, publish the following via MQTT:
(replace < VIN > with the VIN of the Car)
```
mz2mqtt/SET/<VIN>/refresh
```

---
### History:

| Date | Change |
|------------|---------------------------------------------------------------------|
| 26.04.2023 | Initial Version |
| 03.06.2023 | only one refresh at the beginning because risk of battery discharge |
| 08.06.2023 | refresh Data via MQTT |

14 changes: 14 additions & 0 deletions config_example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mqtt:
host: # your mqtthost
port: 1883 # your mqtt port - default 1883
user: # your mqtt user or empty when no authentication
password: # your mqtt password
topic: mz2mqtt # your mqtt topic - default mz2mqtt. Leave it default for evcc
clientname: mz2mqtt # your mqtt clientname - default mz2mqtt
mazda:
user: # email address from your Mazda account
password: # password from your Mazda account
region: MME # your Region (MNAO:North America, MME:Europe, MJO:Japan) - default MME
status:
wait: 30 # wait time in minutes getting status - default 30
refreshwait: 2 # wait time in minutes after refresh data - default 2
140 changes: 140 additions & 0 deletions mz2mqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import asyncio
import logging
from queue import SimpleQueue

import paho.mqtt.client as mqtt_client
import mzlib
import yaml

async def main() -> None:

cmd_queue = SimpleQueue()

def create_msg(object, vehicleid, mqtt_topic, indent='/'):
for key in object:
if type(object[key]) == dict:
create_msg(object[key], vehicleid, mqtt_topic, indent + key + '/')
else:
mqttc.publish(mqtt_topic + '/' + str(vehicleid) + indent + key, object[key], 0, True)
return

async def get_and_publish(vehicle):
logging.info('get and publish data for ' + vehicle['vin'])
vehicle_status = await mazda_client.get_vehicle_status(vehicle['id'])
create_msg(vehicle_status, vehicle['vin'], mqtt_topic)
if vehicle['isElectric']:
vehicle_ev_status = await mazda_client.get_ev_vehicle_status(vehicle['id'])
create_msg(vehicle_ev_status, vehicle['vin'], mqtt_topic)

def on_connect(client, userdata, flags, rc):
if rc == 0:
logging.info("MQTT connected OK")
else:
logging.error("Bad connection Returned code=", rc)

def on_message(mosq, obj, msg):
msg.payload = str(msg.payload)
logging.info("received " + msg.topic + " " + msg.payload)
mqtt_cmd = msg.topic.split("/")
if mqtt_cmd[1].upper() == "SET":
cmd_queue.put(mqtt_cmd[2] + ':' + mqtt_cmd[3] + ':' + msg.payload)

logger = logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=logging.INFO)

# Read Config
with open('config.yaml', 'r') as configfile:
config = yaml.safe_load(configfile)

# Connect to myMazda
logging.info('Initialize myMazda')
mazda_user = config['mazda']['user']
mazda_password = config['mazda']['password']
mazda_region = config['mazda']['region'] or 'MME'
mazda_client = mzlib.Client(mazda_user, mazda_password, mazda_region, use_cached_vehicle_list=True)

status_wait = (config['status']['wait'] or 30) * 12
status_refreshwait = (config['status']['refreshwait'] or 2) * 60

# Connect to MQTT-Broker
logging.info('Initalize MQTT')
mqtt_broker_address = config['mqtt']['host']
mqtt_broker_port = config['mqtt']['port'] or 1883
mqtt_broker_user = config['mqtt']['user'] or None
mqtt_broker_password = config['mqtt']['password'] or None
mqtt_topic = config['mqtt']['topic'] or 'mz2mqtt'
mqtt_clientname = config['mqtt']['clientname'] or 'mz2mqtt'

mqttc = mqtt_client.Client(mqtt_clientname)
mqttc.enable_logger(logger)
mqttc.on_connect = on_connect
mqttc.on_message = on_message
mqttc.username_pw_set(username=mqtt_broker_user, password=mqtt_broker_password)
mqttc.connect(mqtt_broker_address, mqtt_broker_port, 60)
mqttc.subscribe(mqtt_topic + '/' + 'SET/#', 0)
mqttc.loop_start()

# Get all Vehicles and publish base
logging.info('Get all vehicles')
try:
vehicles = await mazda_client.get_vehicles()
except Exception:
raise Exception("Failed to get list of vehicles")

# Publish vehicle data
logging.info('publish all vehicles base data')
for vehicle in vehicles:
create_msg(vehicle,vehicle['vin'], mqtt_topic)

# refresh all vehicle data at startup
try:
for vehicle in vehicles:
logging.info('refresh data for ' + vehicle['vin'])
await mazda_client.refresh_vehicle_status(vehicle['id'])
except:
logging.error('can not refresh all vehicles data')
await mazda_client.close()

logging.info('wait ' + str(status_refreshwait) + 's for data after refresh')
await asyncio.sleep(status_refreshwait)

# Main loop
try:
count = 0
while True:
# look for new API input
while not cmd_queue.empty():
r = cmd_queue.get_nowait()
mqtt_cmd = r.split(':')
match mqtt_cmd[1]:
case 'refresh':
found = False
for vehicle in vehicles:
if vehicle['vin'] == mqtt_cmd[0]:
found = True
logging.info('send refresh for ' + vehicle['vin'] + ' and wait' + str(status_refreshwait) + 's')
await mazda_client.refresh_vehicle_status(vehicle['id'])
await asyncio.sleep(status_refreshwait)
await get_and_publish(vehicle)
if not found:
logging.error('VIN ' + mqtt_cmd[0] + ' not found')
case _:
logging.error("invalid command: " + mqtt_cmd[1])

# wait time reached and get data
if count == 0:
for vehicle in vehicles:
await get_and_publish(vehicle)
count = status_wait

count -= 1
await asyncio.sleep(5)
except:
# Close the session
mqttc.loop_stop()
await mazda_client.close()


if __name__ == "__main__":
asyncio.run(main())
59 changes: 59 additions & 0 deletions mz2mqtt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
template: mz2mqtt
products:
- description:
generic: mz2mqtt
group: generic
requirements:
description:
en: myMazda to MQTT. Required MQTT broker configuration and a mz2mqtt installation https://github.com/C64Axel/mz2mqtt.
de: myMazda zu MQTT. Voraussetzung ist ein konfigurierter MQTT Broker und eine mz2mqtt Installation https://github.com/C64Axel/mz2mqtt.
params:
- name: title
- name: vin
required: true
help:
de: Erforderlich
en: Required
- name: capacity
- name: phases
advanced: true
- name: icon
default: car
advanced: true
- name: timeout
default: 720h
advanced: true
- preset: vehicle-identify
render: |
type: custom
{{- if .title }}
title: {{ .title }}
{{- end }}
{{- if .icon }}
icon: {{ .icon }}
{{- end }}
{{- if .capacity }}
capacity: {{ .capacity }}
{{- end }}
{{- if .phases }}
phases: {{ .phases }}
{{- end }}
{{- include "vehicle-identify" . }}
soc:
source: mqtt
topic: mz2mqtt/{{ .vin }}/chargeInfo/batteryLevelPercentage
timeout: {{ .timeout }}
status:
source: combined
plugged:
source: mqtt
topic: mz2mqtt/{{ .vin }}/chargeInfo/pluggedIn
timeout: {{ .timeout }}
charging:
source: mqtt
topic: mz2mqtt/{{ .vin }}/chargeInfo/charging
timeout: {{ .timeout }}
range:
source: mqtt
topic: mz2mqtt/{{ .vin }}/chargeInfo/drivingRangeKm
timeout: {{ .timeout }}
9 changes: 9 additions & 0 deletions mzlib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from mzlib.client import Client
from mzlib.exceptions import (
MazdaException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaLoginFailedException
)
Loading

0 comments on commit 7ec5e04

Please sign in to comment.