Home Assistant Git Exporter
This commit is contained in:
340
config/custom_components/ecoflow_cloud/mqtt/ecoflow_mqtt.py
Normal file
340
config/custom_components/ecoflow_cloud/mqtt/ecoflow_mqtt.py
Normal file
@@ -0,0 +1,340 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import ssl
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import paho.mqtt.client as mqtt_client
|
||||
import requests
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, DOMAIN
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.util import utcnow
|
||||
from reactivex import Subject, Observable
|
||||
|
||||
from .proto import powerstream_pb2 as powerstream, ecopacket_pb2 as ecopacket
|
||||
from .utils import BoundFifoList
|
||||
from ..config.const import CONF_DEVICE_TYPE, CONF_DEVICE_ID, OPTS_REFRESH_PERIOD_SEC, EcoflowModel
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EcoflowException(Exception):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(args, kwargs)
|
||||
|
||||
|
||||
class EcoflowAuthentication:
|
||||
def __init__(self, ecoflow_username, ecoflow_password):
|
||||
self.ecoflow_username = ecoflow_username
|
||||
self.ecoflow_password = ecoflow_password
|
||||
self.user_id = None
|
||||
self.token = None
|
||||
self.mqtt_url = "mqtt.mqtt.com"
|
||||
self.mqtt_port = 8883
|
||||
self.mqtt_username = None
|
||||
self.mqtt_password = None
|
||||
|
||||
def authorize(self):
|
||||
url = "https://api.ecoflow.com/auth/login"
|
||||
headers = {"lang": "en_US", "content-type": "application/json"}
|
||||
data = {"email": self.ecoflow_username,
|
||||
"password": base64.b64encode(self.ecoflow_password.encode()).decode(),
|
||||
"scene": "IOT_APP",
|
||||
"userType": "ECOFLOW"}
|
||||
|
||||
_LOGGER.info(f"Login to EcoFlow API {url}")
|
||||
request = requests.post(url, json=data, headers=headers)
|
||||
response = self.get_json_response(request)
|
||||
|
||||
try:
|
||||
self.token = response["data"]["token"]
|
||||
self.user_id = response["data"]["user"]["userId"]
|
||||
user_name = response["data"]["user"].get("name", "<no user name>")
|
||||
except KeyError as key:
|
||||
raise EcoflowException(f"Failed to extract key {key} from response: {response}")
|
||||
|
||||
_LOGGER.info(f"Successfully logged in: {user_name}")
|
||||
|
||||
url = "https://api.ecoflow.com/iot-auth/app/certification"
|
||||
headers = {"lang": "en_US", "authorization": f"Bearer {self.token}"}
|
||||
data = {"userId": self.user_id}
|
||||
|
||||
_LOGGER.info(f"Requesting IoT MQTT credentials {url}")
|
||||
request = requests.get(url, data=data, headers=headers)
|
||||
response = self.get_json_response(request)
|
||||
|
||||
try:
|
||||
self.mqtt_url = response["data"]["url"]
|
||||
self.mqtt_port = int(response["data"]["port"])
|
||||
self.mqtt_username = response["data"]["certificateAccount"]
|
||||
self.mqtt_password = response["data"]["certificatePassword"]
|
||||
except KeyError as key:
|
||||
raise EcoflowException(f"Failed to extract key {key} from {response}")
|
||||
|
||||
_LOGGER.info(f"Successfully extracted account: {self.mqtt_username}")
|
||||
|
||||
def get_json_response(self, request):
|
||||
if request.status_code != 200:
|
||||
raise EcoflowException(f"Got HTTP status code {request.status_code}: {request.text}")
|
||||
|
||||
try:
|
||||
response = json.loads(request.text)
|
||||
response_message = response["message"]
|
||||
except KeyError as key:
|
||||
raise EcoflowException(f"Failed to extract key {key} from {response}")
|
||||
except Exception as error:
|
||||
raise EcoflowException(f"Failed to parse response: {request.text} Error: {error}")
|
||||
|
||||
if response_message.lower() != "success":
|
||||
raise EcoflowException(f"{response_message}")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class EcoflowDataHolder:
|
||||
def __init__(self, update_period_sec: int, collect_raw: bool = False):
|
||||
self.__update_period_sec = update_period_sec
|
||||
self.__collect_raw = collect_raw
|
||||
self.set = BoundFifoList[dict[str, Any]]()
|
||||
self.set_reply = BoundFifoList[dict[str, Any]]()
|
||||
self.get = BoundFifoList[dict[str, Any]]()
|
||||
self.get_reply = BoundFifoList[dict[str, Any]]()
|
||||
self.params = dict[str, Any]()
|
||||
|
||||
self.raw_data = BoundFifoList[dict[str, Any]]()
|
||||
|
||||
self.__params_time = utcnow().replace(year=2000, month=1, day=1, hour=0, minute=0, second=0)
|
||||
self.__params_broadcast_time = utcnow().replace(year=2000, month=1, day=1, hour=0, minute=0, second=0)
|
||||
self.__params_observable = Subject[dict[str, Any]]()
|
||||
|
||||
self.__set_reply_observable = Subject[list[dict[str, Any]]]()
|
||||
self.__get_reply_observable = Subject[list[dict[str, Any]]]()
|
||||
|
||||
def params_observable(self) -> Observable[dict[str, Any]]:
|
||||
return self.__params_observable
|
||||
|
||||
def get_reply_observable(self) -> Observable[list[dict[str, Any]]]:
|
||||
return self.__get_reply_observable
|
||||
|
||||
def set_reply_observable(self) -> Observable[list[dict[str, Any]]]:
|
||||
return self.__set_reply_observable
|
||||
|
||||
def add_set_message(self, msg: dict[str, Any]):
|
||||
self.set.append(msg)
|
||||
|
||||
def add_set_reply_message(self, msg: dict[str, Any]):
|
||||
self.set_reply.append(msg)
|
||||
self.__set_reply_observable.on_next(self.set_reply)
|
||||
|
||||
def add_get_message(self, msg: dict[str, Any]):
|
||||
self.get.append(msg)
|
||||
|
||||
def add_get_reply_message(self, msg: dict[str, Any]):
|
||||
self.get_reply.append(msg)
|
||||
self.__get_reply_observable.on_next(self.get_reply)
|
||||
|
||||
def update_to_target_state(self, target_state: dict[str, Any]):
|
||||
self.params.update(target_state)
|
||||
self.__broadcast()
|
||||
|
||||
def update_data(self, raw: dict[str, Any]):
|
||||
self.__add_raw_data(raw)
|
||||
# self.__params_time = datetime.fromtimestamp(raw['timestamp'], UTC)
|
||||
self.__params_time = utcnow()
|
||||
self.params['timestamp'] = raw['timestamp']
|
||||
self.params.update(raw['params'])
|
||||
|
||||
if (utcnow() - self.__params_broadcast_time).total_seconds() > self.__update_period_sec:
|
||||
self.__broadcast()
|
||||
|
||||
def __broadcast(self):
|
||||
self.__params_broadcast_time = utcnow()
|
||||
self.__params_observable.on_next(self.params)
|
||||
|
||||
def __add_raw_data(self, raw: dict[str, Any]):
|
||||
if self.__collect_raw:
|
||||
self.raw_data.append(raw)
|
||||
|
||||
def params_time(self) -> datetime:
|
||||
return self.__params_time
|
||||
|
||||
|
||||
class EcoflowMQTTClient:
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, auth: EcoflowAuthentication):
|
||||
|
||||
self.auth = auth
|
||||
self.config_entry = entry
|
||||
self.device_type = entry.data[CONF_DEVICE_TYPE]
|
||||
self.device_sn = entry.data[CONF_DEVICE_ID]
|
||||
|
||||
self._data_topic = f"/app/device/property/{self.device_sn}"
|
||||
self._set_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/set"
|
||||
self._set_reply_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/set_reply"
|
||||
self._get_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/get"
|
||||
self._get_reply_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/get_reply"
|
||||
|
||||
self.data = EcoflowDataHolder(entry.options.get(OPTS_REFRESH_PERIOD_SEC), self.device_type == "DIAGNOSTIC")
|
||||
|
||||
self.device_info_main = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.device_sn)},
|
||||
manufacturer="EcoFlow",
|
||||
name=entry.title,
|
||||
model=self.device_type,
|
||||
)
|
||||
|
||||
self.client = mqtt_client.Client(client_id=f'ANDROID_-{str(uuid.uuid4()).upper()}_{auth.user_id}',
|
||||
clean_session=True, reconnect_on_failure=True)
|
||||
self.client.username_pw_set(self.auth.mqtt_username, self.auth.mqtt_password)
|
||||
self.client.tls_set(certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED)
|
||||
self.client.tls_insecure_set(False)
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
if self.device_type == EcoflowModel.POWERSTREAM.name:
|
||||
self.client.on_message = self.on_bytes_message
|
||||
else:
|
||||
self.client.on_message = self.on_json_message
|
||||
|
||||
_LOGGER.info(f"Connecting to MQTT Broker {self.auth.mqtt_url}:{self.auth.mqtt_port}")
|
||||
self.client.connect(self.auth.mqtt_url, self.auth.mqtt_port, 30)
|
||||
self.client.loop_start()
|
||||
|
||||
def is_connected(self):
|
||||
return self.client.is_connected()
|
||||
|
||||
def reconnect(self) -> bool:
|
||||
try:
|
||||
_LOGGER.info(f"Re-connecting to MQTT Broker {self.auth.mqtt_url}:{self.auth.mqtt_port}")
|
||||
self.client.loop_stop(True)
|
||||
self.client.reconnect()
|
||||
self.client.loop_start()
|
||||
return True
|
||||
except Exception as e:
|
||||
_LOGGER.error(e)
|
||||
return False
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
match rc:
|
||||
case 0:
|
||||
self.client.subscribe([(self._data_topic, 1),
|
||||
(self._set_topic, 1), (self._set_reply_topic, 1),
|
||||
(self._get_topic, 1), (self._get_reply_topic, 1)])
|
||||
_LOGGER.info(f"Subscribed to MQTT topic {self._data_topic}")
|
||||
case -1:
|
||||
_LOGGER.error("Failed to connect to MQTT: connection timed out")
|
||||
case 1:
|
||||
_LOGGER.error("Failed to connect to MQTT: incorrect protocol version")
|
||||
case 2:
|
||||
_LOGGER.error("Failed to connect to MQTT: invalid client identifier")
|
||||
case 3:
|
||||
_LOGGER.error("Failed to connect to MQTT: server unavailable")
|
||||
case 4:
|
||||
_LOGGER.error("Failed to connect to MQTT: bad username or password")
|
||||
case 5:
|
||||
_LOGGER.error("Failed to connect to MQTT: not authorised")
|
||||
case _:
|
||||
_LOGGER.error(f"Failed to connect to MQTT: another error occured: {rc}")
|
||||
|
||||
return client
|
||||
|
||||
def on_disconnect(self, client, userdata, rc):
|
||||
if rc != 0:
|
||||
_LOGGER.error(f"Unexpected MQTT disconnection: {rc}. Will auto-reconnect")
|
||||
time.sleep(5)
|
||||
# self.client.reconnect() ??
|
||||
|
||||
def on_json_message(self, client, userdata, message):
|
||||
try:
|
||||
payload = message.payload.decode("utf-8", errors='ignore')
|
||||
raw = json.loads(payload)
|
||||
|
||||
if message.topic == self._data_topic:
|
||||
self.data.update_data(raw)
|
||||
elif message.topic == self._set_topic:
|
||||
self.data.add_set_message(raw)
|
||||
elif message.topic == self._set_reply_topic:
|
||||
self.data.add_set_reply_message(raw)
|
||||
elif message.topic == self._get_topic:
|
||||
self.data.add_get_message(raw)
|
||||
elif message.topic == self._get_reply_topic:
|
||||
self.data.add_get_reply_message(raw)
|
||||
except UnicodeDecodeError as error:
|
||||
_LOGGER.error(f"UnicodeDecodeError: {error}. Ignoring message and waiting for the next one.")
|
||||
|
||||
def on_bytes_message(self, client, userdata, message):
|
||||
try:
|
||||
payload = message.payload
|
||||
|
||||
while True:
|
||||
packet = ecopacket.SendHeaderMsg()
|
||||
packet.ParseFromString(payload)
|
||||
|
||||
_LOGGER.debug("cmd id %u payload \"%s\"", packet.msg.cmd_id, payload.hex())
|
||||
|
||||
if packet.msg.cmd_id != 1:
|
||||
_LOGGER.info("Unsupported EcoPacket cmd id %u", packet.msg.cmd_id)
|
||||
|
||||
else:
|
||||
heartbeat = powerstream.InverterHeartbeat()
|
||||
heartbeat.ParseFromString(packet.msg.pdata)
|
||||
|
||||
raw = {"params": {}}
|
||||
|
||||
for descriptor in heartbeat.DESCRIPTOR.fields:
|
||||
if not heartbeat.HasField(descriptor.name):
|
||||
continue
|
||||
|
||||
raw["params"][descriptor.name] = getattr(heartbeat, descriptor.name)
|
||||
|
||||
_LOGGER.info("Found %u fields", len(raw["params"]))
|
||||
|
||||
raw["timestamp"] = utcnow()
|
||||
|
||||
self.data.update_data(raw)
|
||||
|
||||
if packet.ByteSize() >= len(payload):
|
||||
break
|
||||
|
||||
_LOGGER.info("Found another frame in payload")
|
||||
|
||||
packetLength = len(payload) - packet.ByteSize()
|
||||
payload = payload[:packetLength]
|
||||
|
||||
except Exception as error:
|
||||
_LOGGER.error(error)
|
||||
_LOGGER.info(message.payload.hex())
|
||||
|
||||
message_id = 999900000 + random.randint(10000, 99999)
|
||||
|
||||
def __prepare_payload(self, command: dict):
|
||||
self.message_id += 1
|
||||
payload = {"from": "HomeAssistant",
|
||||
"id": f"{self.message_id}",
|
||||
"version": "1.0"}
|
||||
payload.update(command)
|
||||
return payload
|
||||
|
||||
def __send(self, topic: str, message: str):
|
||||
try:
|
||||
info = self.client.publish(topic, message, 1)
|
||||
_LOGGER.debug("Sending " + message + " :" + str(info) + "(" + str(info.is_published()) + ")")
|
||||
except RuntimeError as error:
|
||||
_LOGGER.error(error)
|
||||
|
||||
def send_get_message(self, command: dict):
|
||||
payload = self.__prepare_payload(command)
|
||||
self.__send(self._get_topic, json.dumps(payload))
|
||||
|
||||
def send_set_message(self, mqtt_state: dict[str, Any], command: dict):
|
||||
self.data.update_to_target_state(mqtt_state)
|
||||
payload = self.__prepare_payload(command)
|
||||
self.__send(self._set_topic, json.dumps(payload))
|
||||
|
||||
def stop(self):
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
@@ -0,0 +1,57 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message Header
|
||||
{
|
||||
optional bytes pdata = 1;
|
||||
optional int32 src = 2;
|
||||
optional int32 dest = 3;
|
||||
optional int32 d_src= 4;
|
||||
optional int32 d_dest = 5;
|
||||
optional int32 enc_type = 6;
|
||||
optional int32 check_type = 7;
|
||||
optional int32 cmd_func = 8;
|
||||
optional int32 cmd_id = 9;
|
||||
optional int32 data_len = 10;
|
||||
optional int32 need_ack = 11;
|
||||
optional int32 is_ack = 12;
|
||||
optional int32 seq = 14;
|
||||
optional int32 product_id = 15;
|
||||
optional int32 version = 16;
|
||||
optional int32 payload_ver = 17;
|
||||
optional int32 time_snap = 18;
|
||||
optional int32 is_rw_cmd = 19;
|
||||
optional int32 is_queue = 20;
|
||||
optional int32 ack_type= 21;
|
||||
optional string code = 22;
|
||||
optional string from = 23;
|
||||
optional string module_sn = 24;
|
||||
optional string device_sn = 25;
|
||||
}
|
||||
|
||||
message SendHeaderMsg
|
||||
{
|
||||
optional Header msg = 1;
|
||||
}
|
||||
|
||||
message SendMsgHart
|
||||
{
|
||||
optional int32 link_id = 1;
|
||||
optional int32 src = 2;
|
||||
optional int32 dest = 3;
|
||||
optional int32 d_src = 4;
|
||||
optional int32 d_dest = 5;
|
||||
optional int32 enc_type = 6;
|
||||
optional int32 check_type = 7;
|
||||
optional int32 cmd_func = 8;
|
||||
optional int32 cmd_id = 9;
|
||||
optional int32 data_len = 10;
|
||||
optional int32 need_ack = 11;
|
||||
optional int32 is_ack = 12;
|
||||
optional int32 ack_type = 13;
|
||||
optional int32 seq = 14;
|
||||
optional int32 time_snap = 15;
|
||||
optional int32 is_rw_cmd = 16;
|
||||
optional int32 is_queue = 17;
|
||||
optional int32 product_id = 18;
|
||||
optional int32 version = 19;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: ecopacket.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x65\x63opacket.proto\"\xb8\x06\n\x06Header\x12\x12\n\x05pdata\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x10\n\x03src\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x11\n\x04\x64\x65st\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x12\n\x05\x64_src\x18\x04 \x01(\x05H\x03\x88\x01\x01\x12\x13\n\x06\x64_dest\x18\x05 \x01(\x05H\x04\x88\x01\x01\x12\x15\n\x08\x65nc_type\x18\x06 \x01(\x05H\x05\x88\x01\x01\x12\x17\n\ncheck_type\x18\x07 \x01(\x05H\x06\x88\x01\x01\x12\x15\n\x08\x63md_func\x18\x08 \x01(\x05H\x07\x88\x01\x01\x12\x13\n\x06\x63md_id\x18\t \x01(\x05H\x08\x88\x01\x01\x12\x15\n\x08\x64\x61ta_len\x18\n \x01(\x05H\t\x88\x01\x01\x12\x15\n\x08need_ack\x18\x0b \x01(\x05H\n\x88\x01\x01\x12\x13\n\x06is_ack\x18\x0c \x01(\x05H\x0b\x88\x01\x01\x12\x10\n\x03seq\x18\x0e \x01(\x05H\x0c\x88\x01\x01\x12\x17\n\nproduct_id\x18\x0f \x01(\x05H\r\x88\x01\x01\x12\x14\n\x07version\x18\x10 \x01(\x05H\x0e\x88\x01\x01\x12\x18\n\x0bpayload_ver\x18\x11 \x01(\x05H\x0f\x88\x01\x01\x12\x16\n\ttime_snap\x18\x12 \x01(\x05H\x10\x88\x01\x01\x12\x16\n\tis_rw_cmd\x18\x13 \x01(\x05H\x11\x88\x01\x01\x12\x15\n\x08is_queue\x18\x14 \x01(\x05H\x12\x88\x01\x01\x12\x15\n\x08\x61\x63k_type\x18\x15 \x01(\x05H\x13\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x16 \x01(\tH\x14\x88\x01\x01\x12\x11\n\x04\x66rom\x18\x17 \x01(\tH\x15\x88\x01\x01\x12\x16\n\tmodule_sn\x18\x18 \x01(\tH\x16\x88\x01\x01\x12\x16\n\tdevice_sn\x18\x19 \x01(\tH\x17\x88\x01\x01\x42\x08\n\x06_pdataB\x06\n\x04_srcB\x07\n\x05_destB\x08\n\x06_d_srcB\t\n\x07_d_destB\x0b\n\t_enc_typeB\r\n\x0b_check_typeB\x0b\n\t_cmd_funcB\t\n\x07_cmd_idB\x0b\n\t_data_lenB\x0b\n\t_need_ackB\t\n\x07_is_ackB\x06\n\x04_seqB\r\n\x0b_product_idB\n\n\x08_versionB\x0e\n\x0c_payload_verB\x0c\n\n_time_snapB\x0c\n\n_is_rw_cmdB\x0b\n\t_is_queueB\x0b\n\t_ack_typeB\x07\n\x05_codeB\x07\n\x05_fromB\x0c\n\n_module_snB\x0c\n\n_device_sn\"2\n\rSendHeaderMsg\x12\x19\n\x03msg\x18\x01 \x01(\x0b\x32\x07.HeaderH\x00\x88\x01\x01\x42\x06\n\x04_msg\"\x93\x05\n\x0bSendMsgHart\x12\x14\n\x07link_id\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x10\n\x03src\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x11\n\x04\x64\x65st\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x12\n\x05\x64_src\x18\x04 \x01(\x05H\x03\x88\x01\x01\x12\x13\n\x06\x64_dest\x18\x05 \x01(\x05H\x04\x88\x01\x01\x12\x15\n\x08\x65nc_type\x18\x06 \x01(\x05H\x05\x88\x01\x01\x12\x17\n\ncheck_type\x18\x07 \x01(\x05H\x06\x88\x01\x01\x12\x15\n\x08\x63md_func\x18\x08 \x01(\x05H\x07\x88\x01\x01\x12\x13\n\x06\x63md_id\x18\t \x01(\x05H\x08\x88\x01\x01\x12\x15\n\x08\x64\x61ta_len\x18\n \x01(\x05H\t\x88\x01\x01\x12\x15\n\x08need_ack\x18\x0b \x01(\x05H\n\x88\x01\x01\x12\x13\n\x06is_ack\x18\x0c \x01(\x05H\x0b\x88\x01\x01\x12\x15\n\x08\x61\x63k_type\x18\r \x01(\x05H\x0c\x88\x01\x01\x12\x10\n\x03seq\x18\x0e \x01(\x05H\r\x88\x01\x01\x12\x16\n\ttime_snap\x18\x0f \x01(\x05H\x0e\x88\x01\x01\x12\x16\n\tis_rw_cmd\x18\x10 \x01(\x05H\x0f\x88\x01\x01\x12\x15\n\x08is_queue\x18\x11 \x01(\x05H\x10\x88\x01\x01\x12\x17\n\nproduct_id\x18\x12 \x01(\x05H\x11\x88\x01\x01\x12\x14\n\x07version\x18\x13 \x01(\x05H\x12\x88\x01\x01\x42\n\n\x08_link_idB\x06\n\x04_srcB\x07\n\x05_destB\x08\n\x06_d_srcB\t\n\x07_d_destB\x0b\n\t_enc_typeB\r\n\x0b_check_typeB\x0b\n\t_cmd_funcB\t\n\x07_cmd_idB\x0b\n\t_data_lenB\x0b\n\t_need_ackB\t\n\x07_is_ackB\x0b\n\t_ack_typeB\x06\n\x04_seqB\x0c\n\n_time_snapB\x0c\n\n_is_rw_cmdB\x0b\n\t_is_queueB\r\n\x0b_product_idB\n\n\x08_versionb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ecopacket_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_globals['_HEADER']._serialized_start=20
|
||||
_globals['_HEADER']._serialized_end=844
|
||||
_globals['_SENDHEADERMSG']._serialized_start=846
|
||||
_globals['_SENDHEADERMSG']._serialized_end=896
|
||||
_globals['_SENDMSGHART']._serialized_start=899
|
||||
_globals['_SENDMSGHART']._serialized_end=1558
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
105
config/custom_components/ecoflow_cloud/mqtt/proto/platform.proto
Normal file
105
config/custom_components/ecoflow_cloud/mqtt/proto/platform.proto
Normal file
@@ -0,0 +1,105 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message EnergyItem
|
||||
{
|
||||
optional uint32 timestamp = 1;
|
||||
optional uint32 watth_type = 2;
|
||||
repeated uint32 watth = 3;
|
||||
}
|
||||
|
||||
message EnergyTotalReport
|
||||
{
|
||||
optional uint32 watth_seq = 1;
|
||||
optional EnergyItem watth_item = 2;
|
||||
}
|
||||
|
||||
message BatchEnergyTotalReport
|
||||
{
|
||||
optional uint32 watth_seq = 1;
|
||||
repeated EnergyItem watth_item = 2;
|
||||
}
|
||||
|
||||
message EnergyTotalReportAck
|
||||
{
|
||||
optional uint32 result = 1;
|
||||
optional uint32 watth_seq = 2;
|
||||
optional uint32 watth_type = 3;
|
||||
}
|
||||
|
||||
message EventRecordItem
|
||||
{
|
||||
optional uint32 timestamp = 1;
|
||||
optional uint32 sys_ms = 2;
|
||||
optional uint32 event_no = 3;
|
||||
repeated float event_detail = 4;
|
||||
}
|
||||
|
||||
message EventRecordReport
|
||||
{
|
||||
optional uint32 event_ver = 1;
|
||||
optional uint32 event_seq = 2;
|
||||
repeated EventRecordItem event_item = 3;
|
||||
}
|
||||
|
||||
message EventInfoReportAck
|
||||
{
|
||||
optional uint32 result = 1;
|
||||
optional uint32 event_seq = 2;
|
||||
optional uint32 event_item_num =3;
|
||||
}
|
||||
|
||||
message ProductNameSet
|
||||
{
|
||||
optional string name = 1;
|
||||
}
|
||||
|
||||
message ProductNameSetAck
|
||||
{
|
||||
optional uint32 result = 1;
|
||||
}
|
||||
|
||||
message ProductNameGet { }
|
||||
|
||||
message ProductNameGetAck
|
||||
{
|
||||
optional string name = 3;
|
||||
}
|
||||
|
||||
message RTCTimeGet { }
|
||||
|
||||
message RTCTimeGetAck
|
||||
{
|
||||
optional uint32 timestamp = 1;
|
||||
optional int32 timezone = 2;
|
||||
}
|
||||
|
||||
message RTCTimeSet
|
||||
{
|
||||
optional uint32 timestamp = 1;
|
||||
optional int32 timezone = 2;
|
||||
}
|
||||
|
||||
message RTCTimeSetAck
|
||||
{
|
||||
optional uint32 result = 1;
|
||||
}
|
||||
|
||||
message country_town_message
|
||||
{
|
||||
optional uint32 country = 1;
|
||||
optional uint32 town = 2;
|
||||
}
|
||||
|
||||
enum PlCmdSets
|
||||
{
|
||||
PL_NONE_CMD_SETS = 0;
|
||||
PL_BASIC_CMD_SETS = 1;
|
||||
PL_EXT_CMD_SETS = 254;
|
||||
}
|
||||
|
||||
enum PlCmdId
|
||||
{
|
||||
PL_CMD_ID_NONE = 0;
|
||||
PL_CMD_ID_XLOG = 16;
|
||||
PL_CMD_ID_WATTH = 32;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: platform.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eplatform.proto\"i\n\nEnergyItem\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x17\n\nwatth_type\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05watth\x18\x03 \x03(\rB\x0c\n\n_timestampB\r\n\x0b_watth_type\"n\n\x11\x45nergyTotalReport\x12\x16\n\twatth_seq\x18\x01 \x01(\rH\x00\x88\x01\x01\x12$\n\nwatth_item\x18\x02 \x01(\x0b\x32\x0b.EnergyItemH\x01\x88\x01\x01\x42\x0c\n\n_watth_seqB\r\n\x0b_watth_item\"_\n\x16\x42\x61tchEnergyTotalReport\x12\x16\n\twatth_seq\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x1f\n\nwatth_item\x18\x02 \x03(\x0b\x32\x0b.EnergyItemB\x0c\n\n_watth_seq\"\x84\x01\n\x14\x45nergyTotalReportAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x16\n\twatth_seq\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x17\n\nwatth_type\x18\x03 \x01(\rH\x02\x88\x01\x01\x42\t\n\x07_resultB\x0c\n\n_watth_seqB\r\n\x0b_watth_type\"\x91\x01\n\x0f\x45ventRecordItem\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x13\n\x06sys_ms\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x08\x65vent_no\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x14\n\x0c\x65vent_detail\x18\x04 \x03(\x02\x42\x0c\n\n_timestampB\t\n\x07_sys_msB\x0b\n\t_event_no\"\x85\x01\n\x11\x45ventRecordReport\x12\x16\n\tevent_ver\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x16\n\tevent_seq\x18\x02 \x01(\rH\x01\x88\x01\x01\x12$\n\nevent_item\x18\x03 \x03(\x0b\x32\x10.EventRecordItemB\x0c\n\n_event_verB\x0c\n\n_event_seq\"\x8a\x01\n\x12\x45ventInfoReportAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x16\n\tevent_seq\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1b\n\x0e\x65vent_item_num\x18\x03 \x01(\rH\x02\x88\x01\x01\x42\t\n\x07_resultB\x0c\n\n_event_seqB\x11\n\x0f_event_item_num\",\n\x0eProductNameSet\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_name\"3\n\x11ProductNameSetAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x42\t\n\x07_result\"\x10\n\x0eProductNameGet\"/\n\x11ProductNameGetAck\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_name\"\x0c\n\nRTCTimeGet\"Y\n\rRTCTimeGetAck\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08timezone\x18\x02 \x01(\x05H\x01\x88\x01\x01\x42\x0c\n\n_timestampB\x0b\n\t_timezone\"V\n\nRTCTimeSet\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08timezone\x18\x02 \x01(\x05H\x01\x88\x01\x01\x42\x0c\n\n_timestampB\x0b\n\t_timezone\"/\n\rRTCTimeSetAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x42\t\n\x07_result\"T\n\x14\x63ountry_town_message\x12\x14\n\x07\x63ountry\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x11\n\x04town\x18\x02 \x01(\rH\x01\x88\x01\x01\x42\n\n\x08_countryB\x07\n\x05_town*N\n\tPlCmdSets\x12\x14\n\x10PL_NONE_CMD_SETS\x10\x00\x12\x15\n\x11PL_BASIC_CMD_SETS\x10\x01\x12\x14\n\x0fPL_EXT_CMD_SETS\x10\xfe\x01*F\n\x07PlCmdId\x12\x12\n\x0ePL_CMD_ID_NONE\x10\x00\x12\x12\n\x0ePL_CMD_ID_XLOG\x10\x10\x12\x13\n\x0fPL_CMD_ID_WATTH\x10 b\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'platform_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_globals['_PLCMDSETS']._serialized_start=1388
|
||||
_globals['_PLCMDSETS']._serialized_end=1466
|
||||
_globals['_PLCMDID']._serialized_start=1468
|
||||
_globals['_PLCMDID']._serialized_end=1538
|
||||
_globals['_ENERGYITEM']._serialized_start=18
|
||||
_globals['_ENERGYITEM']._serialized_end=123
|
||||
_globals['_ENERGYTOTALREPORT']._serialized_start=125
|
||||
_globals['_ENERGYTOTALREPORT']._serialized_end=235
|
||||
_globals['_BATCHENERGYTOTALREPORT']._serialized_start=237
|
||||
_globals['_BATCHENERGYTOTALREPORT']._serialized_end=332
|
||||
_globals['_ENERGYTOTALREPORTACK']._serialized_start=335
|
||||
_globals['_ENERGYTOTALREPORTACK']._serialized_end=467
|
||||
_globals['_EVENTRECORDITEM']._serialized_start=470
|
||||
_globals['_EVENTRECORDITEM']._serialized_end=615
|
||||
_globals['_EVENTRECORDREPORT']._serialized_start=618
|
||||
_globals['_EVENTRECORDREPORT']._serialized_end=751
|
||||
_globals['_EVENTINFOREPORTACK']._serialized_start=754
|
||||
_globals['_EVENTINFOREPORTACK']._serialized_end=892
|
||||
_globals['_PRODUCTNAMESET']._serialized_start=894
|
||||
_globals['_PRODUCTNAMESET']._serialized_end=938
|
||||
_globals['_PRODUCTNAMESETACK']._serialized_start=940
|
||||
_globals['_PRODUCTNAMESETACK']._serialized_end=991
|
||||
_globals['_PRODUCTNAMEGET']._serialized_start=993
|
||||
_globals['_PRODUCTNAMEGET']._serialized_end=1009
|
||||
_globals['_PRODUCTNAMEGETACK']._serialized_start=1011
|
||||
_globals['_PRODUCTNAMEGETACK']._serialized_end=1058
|
||||
_globals['_RTCTIMEGET']._serialized_start=1060
|
||||
_globals['_RTCTIMEGET']._serialized_end=1072
|
||||
_globals['_RTCTIMEGETACK']._serialized_start=1074
|
||||
_globals['_RTCTIMEGETACK']._serialized_end=1163
|
||||
_globals['_RTCTIMESET']._serialized_start=1165
|
||||
_globals['_RTCTIMESET']._serialized_end=1251
|
||||
_globals['_RTCTIMESETACK']._serialized_start=1253
|
||||
_globals['_RTCTIMESETACK']._serialized_end=1300
|
||||
_globals['_COUNTRY_TOWN_MESSAGE']._serialized_start=1302
|
||||
_globals['_COUNTRY_TOWN_MESSAGE']._serialized_end=1386
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,127 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message InverterHeartbeat {
|
||||
optional uint32 inv_error_code = 1;
|
||||
optional uint32 inv_warning_code = 3;
|
||||
optional uint32 pv1_error_code = 2;
|
||||
optional uint32 pv1_warning_code = 4;
|
||||
optional uint32 pv2_error_code = 5;
|
||||
optional uint32 pv2_warning_code = 6;
|
||||
optional uint32 bat_error_code = 7;
|
||||
optional uint32 bat_warning_code = 8;
|
||||
optional uint32 llc_error_code = 9;
|
||||
optional uint32 llc_warning_code = 10;
|
||||
optional uint32 pv1_status = 11;
|
||||
optional uint32 pv2_status = 12;
|
||||
optional uint32 bat_status = 13;
|
||||
optional uint32 llc_status = 14;
|
||||
optional uint32 inv_status = 15;
|
||||
optional int32 pv1_input_volt = 16;
|
||||
optional int32 pv1_op_volt = 17;
|
||||
optional int32 pv1_input_cur = 18;
|
||||
optional int32 pv1_input_watts = 19;
|
||||
optional int32 pv1_temp = 20;
|
||||
optional int32 pv2_input_volt = 21;
|
||||
optional int32 pv2_op_volt = 22;
|
||||
optional int32 pv2_input_cur = 23;
|
||||
optional int32 pv2_input_watts = 24;
|
||||
optional int32 pv2_temp = 25;
|
||||
optional int32 bat_input_volt = 26;
|
||||
optional int32 bat_op_volt = 27;
|
||||
optional int32 bat_input_cur = 28;
|
||||
optional int32 bat_input_watts = 29;
|
||||
optional int32 bat_temp = 30;
|
||||
optional uint32 bat_soc = 31;
|
||||
optional int32 llc_input_volt = 32;
|
||||
optional int32 llc_op_volt = 33;
|
||||
optional int32 llc_temp = 34;
|
||||
optional int32 inv_input_volt = 35;
|
||||
optional int32 inv_op_volt = 36;
|
||||
optional int32 inv_output_cur = 37;
|
||||
optional int32 inv_output_watts = 38;
|
||||
optional int32 inv_temp = 39;
|
||||
optional int32 inv_freq = 40;
|
||||
optional int32 inv_dc_cur = 41;
|
||||
optional int32 bp_type = 42;
|
||||
optional int32 inv_relay_status = 43;
|
||||
optional int32 pv1_relay_status = 44;
|
||||
optional int32 pv2_relay_status = 45;
|
||||
optional uint32 install_country = 46;
|
||||
optional uint32 install_town = 47;
|
||||
optional uint32 permanent_watts = 48;
|
||||
optional uint32 dynamic_watts = 49;
|
||||
optional uint32 supply_priority = 50;
|
||||
optional uint32 lower_limit = 51;
|
||||
optional uint32 upper_limit = 52;
|
||||
optional uint32 inv_on_off = 53;
|
||||
optional uint32 wireless_error_code = 54;
|
||||
optional uint32 wireless_warning_code = 55;
|
||||
optional uint32 inv_brightness = 56;
|
||||
optional uint32 heartbeat_frequency = 57;
|
||||
optional uint32 rated_power = 58;
|
||||
optional uint32 battery_charge_remain = 59;
|
||||
optional uint32 battery_discharge_remain = 60;
|
||||
}
|
||||
|
||||
message PermanentWattsPack
|
||||
{
|
||||
optional uint32 permanent_watts = 1;
|
||||
}
|
||||
|
||||
message SupplyPriorityPack
|
||||
{
|
||||
optional uint32 supply_priority = 1;
|
||||
}
|
||||
|
||||
message BatLowerPack
|
||||
{
|
||||
optional int32 lower_limit = 1;
|
||||
}
|
||||
|
||||
message BatUpperPack
|
||||
{
|
||||
optional int32 upper_limit = 1;
|
||||
}
|
||||
|
||||
message BrightnessPack
|
||||
{
|
||||
optional int32 brightness = 1;
|
||||
}
|
||||
|
||||
message PowerItem
|
||||
{
|
||||
optional uint32 timestamp = 1;
|
||||
optional sint32 timezone = 2;
|
||||
optional uint32 inv_to_grid_power = 3;
|
||||
optional uint32 inv_to_plug_power = 4;
|
||||
optional int32 battery_power = 5;
|
||||
optional uint32 pv1_output_power = 6;
|
||||
optional uint32 pv2_output_power = 7;
|
||||
}
|
||||
|
||||
message PowerPack
|
||||
{
|
||||
optional uint32 sys_seq = 1;
|
||||
repeated PowerItem sys_power_stream = 2;
|
||||
}
|
||||
|
||||
message PowerAckPack
|
||||
{
|
||||
optional uint32 sys_seq = 1;
|
||||
}
|
||||
|
||||
message NodeMassage
|
||||
{
|
||||
optional string sn = 1;
|
||||
optional bytes mac = 2;
|
||||
}
|
||||
|
||||
message MeshChildNodeInfo
|
||||
{
|
||||
optional uint32 topology_type = 1;
|
||||
optional uint32 mesh_protocol = 2;
|
||||
optional uint32 max_sub_device_num = 3;
|
||||
optional bytes parent_mac_id = 4;
|
||||
optional bytes mesh_id = 5;
|
||||
repeated NodeMassage sub_device_list = 6;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
34
config/custom_components/ecoflow_cloud/mqtt/utils.py
Normal file
34
config/custom_components/ecoflow_cloud/mqtt/utils.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from collections import OrderedDict
|
||||
from typing import Callable, List, TypeVar
|
||||
|
||||
|
||||
class LimitedSizeOrderedDict(OrderedDict):
|
||||
def __init__(self, maxlen=20):
|
||||
"""Initialize a new DedupStore."""
|
||||
super().__init__()
|
||||
self.maxlen = maxlen
|
||||
|
||||
def append(self, key, val, on_delete: Callable = None):
|
||||
self[key] = val
|
||||
self.move_to_end(key)
|
||||
if len(self) > self.maxlen:
|
||||
# Removes the first record which should also be the oldest
|
||||
itm = self.popitem(last=False)
|
||||
if on_delete:
|
||||
on_delete(itm)
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class BoundFifoList(List):
|
||||
|
||||
def __init__(self, maxlen=20) -> None:
|
||||
super().__init__()
|
||||
self.maxlen = maxlen
|
||||
|
||||
def append(self, __object: _T) -> None:
|
||||
super().insert(0, __object)
|
||||
while len(self) >= self.maxlen:
|
||||
self.pop()
|
||||
|
||||
Reference in New Issue
Block a user