diff --git a/ABOUT_THIS_TEMPLATE.md b/ABOUT_THIS_TEMPLATE.md deleted file mode 100644 index c3f1d2b..0000000 --- a/ABOUT_THIS_TEMPLATE.md +++ /dev/null @@ -1,198 +0,0 @@ -# About this template - -Hi, I created this template to help you get started with a new project. - -I have created and maintained a number of python libraries, applications and -frameworks and during those years I have learned a lot about how to create a -project structure and how to structure a project to be as modular and simple -as possible. - -Some decisions I have made while creating this template are: - - - Create a project structure that is as modular as possible. - - Keep it simple and easy to maintain. - - Allow for a lot of flexibility and customizability. - - Low dependency (this template doesn't add dependencies) - -## Structure - -Lets take a look at the structure of this template: - -```text -├── Containerfile # The file to build a container using buildah or docker -├── CONTRIBUTING.md # Onboarding instructions for new contributors -├── docs # Documentation site (add more .md files here) -│   └── index.md # The index page for the docs site -├── .github # Github metadata for repository -│   ├── release_message.sh # A script to generate a release message -│   └── workflows # The CI pipeline for Github Actions -├── .gitignore # A list of files to ignore when pushing to Github -├── HISTORY.md # Auto generated list of changes to the project -├── LICENSE # The license for the project -├── Makefile # A collection of utilities to manage the project -├── MANIFEST.in # A list of files to include in a package -├── mkdocs.yml # Configuration for documentation site -├── dometic_cfx3 # The main python package for the project -│   ├── base.py # The base module for the project -│   ├── __init__.py # This tells Python that this is a package -│   ├── __main__.py # The entry point for the project -│   └── VERSION # The version for the project is kept in a static file -├── README.md # The main readme for the project -├── setup.py # The setup.py file for installing and packaging the project -├── requirements.txt # An empty file to hold the requirements for the project -├── requirements-test.txt # List of requirements for testing and devlopment -├── setup.py # The setup.py file for installing and packaging the project -└── tests # Unit tests for the project (add mote tests files here) - ├── conftest.py # Configuration, hooks and fixtures for pytest - ├── __init__.py # This tells Python that this is a test package - └── test_base.py # The base test case for the project -``` - -## FAQ - -Frequent asked questions. - -### Why this template is not using [Poetry](https://python-poetry.org/) ? - -I really like Poetry and I think it is a great tool to manage your python projects, -if you want to switch to poetry, you can run `make switch-to-poetry`. - -But for this template I wanted to keep it simple. - -Setuptools is the most simple and well supported way of packaging a Python project, -it doesn't require extra dependencies and is the easiest way to install the project. - -Also, poetry doesn't have a good support for installing projects in development mode yet. - -### Why the `requirements.txt` is empty ? - -This template is a low dependency project, so it doesn't have any extra dependencies. -You can add new dependencies as you will or you can use the `make init` command to -generate a `requirements.txt` file based on the template you choose `flask, fastapi, click etc`. - -### Why there is a `requirements-test.txt` file ? - -This file lists all the requirements for testing and development, -I think the development environment and testing environment should be as similar as possible. - -Except those tools that are up to the developer choice (like ipython, ipdb etc). - -### Why the template doesn't have a `pyproject.toml` file ? - -It is possible to run `pip install https://github.com/name/repo/tarball/main` and -have pip to download the package direcly from Git repo. - -For that to work you need to have a `setup.py` file, and `pyproject.toml` is not -supported for that kind of installation. - -I think it is easier for example you want to install specific branch or tag you can -do `pip install https://github.com/name/repo/tarball/{TAG|REVISON|COMMIT}` - -People automating CI for your project will be grateful for having a setup.py file - -### Why isn't this template made as a cookiecutter template? - -I really like [cookiecutter](https://github.com/cookiecutter/cookiecutter) and it is a great way to create new projects, -but for this template I wanted to use the Github `Use this template` button, -to use this template doesn't require to install extra tooling such as cookiecutter. - -Just click on [Use this template](https://github.com/rochacbruno/python-project-template/generate) and you are good to go. - -The substituions are done using github actions and a simple sed script. - -### Why `VERSION` is kept in a static plain text file? - -I used to have my version inside my main module in a `__version__` variable, then -I had to do some tricks to read that version variable inside the setuptools -`setup.py` file because that would be available only after the installation. - -I decided to keep the version in a static file because it is easier to read from -wherever I want without the need to install the package. - -e.g: `cat dometic_cfx3/VERSION` will get the project version without harming -with module imports or anything else, it is useful for CI, logs and debugging. - -### Why to include `tests`, `history` and `Containerfile` as part of the release? - -The `MANIFEST.in` file is used to include the files in the release, once the -project is released to PyPI all the files listed on MANIFEST.in will be included -even if the files are static or not related to Python. - -Some build systems such as RPM, DEB, AUR for some Linux distributions, and also -internal repackaging systems tends to run the tests before the packaging is performed. - -The Containerfile can be useful to provide a safer execution environment for -the project when running on a testing environment. - -I added those files to make it easier for packaging in different formats. - -### Why conftest includes a go_to_tmpdir fixture? - -When your project deals with file system operations, it is a good idea to use -a fixture to create a temporary directory and then remove it after the test. - -Before executing each test pytest will create a temporary directory and will -change the working directory to that path and run the test. - -So the test can create temporary artifacts isolated from other tests. - -After the execution Pytest will remove the temporary directory. - -### Why this template is not using [pre-commit](https://pre-commit.com/) ? - -pre-commit is an excellent tool to automate checks and formatting on your code. - -However I figured out that pre-commit adds extra dependency and it an entry barrier -for new contributors. - -Having the linting, checks and formatting as simple commands on the [Makefile](Makefile) -makes it easier to undestand and change. - -Once the project is bigger and complex, having pre-commit as a dependency can be a good idea. - -### Why the CLI is not using click? - -I wanted to provide a simple template for a CLI application on the project main entry point -click and typer are great alternatives but are external dependencies and this template -doesn't add dependencies besides those used for development. - -### Why this doesn't provide a full example of application using Flask or Django? - -as I said before, I want it to be simple and multipurpose, so I decided to not include -external dependencies and programming design decisions. - -It is up to you to decide if you want to use Flask or Django and to create your application -the way you think is best. - -This template provides utilities in the Makefile to make it easier to you can run: - -```bash -$ make init -Which template do you want to apply? [flask, fastapi, click, typer]? > flask -Generating a new project with Flask ... -``` - -Then the above will download the Flask template and apply it to the project. - -## The Makefile - -All the utilities for the template and project are on the Makefile - -```bash -❯ make -Usage: make - -Targets: -help: ## Show the help. -install: ## Install the project in dev mode. -fmt: ## Format code using black & isort. -lint: ## Run pep8, black, mypy linters. -test: lint ## Run tests and generate coverage report. -watch: ## Run tests on every change. -clean: ## Clean unused files. -virtualenv: ## Create a virtual environment. -release: ## Create a new tag for release. -docs: ## Build the documentation. -switch-to-poetry: ## Switch to poetry package manager. -init: ## Initialize the project based on an application template. -``` diff --git a/HISTORY.md b/HISTORY.md index 9bf6ef0..8db6588 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,13 +1,6 @@ Changelog ========= - -0.1.2 (2021-08-14) ------------------- -- Fix release, README and windows CI. [Bruno Rocha] -- Release: version 0.1.0. [Bruno Rocha] - - -0.1.0 (2021-08-14) +0.1.0 (2023-08-29) ------------------ -- Add release command. [Bruno Rocha] +- Basic Wifi client diff --git a/dometic_cfx3/base.py b/dometic_cfx3/base.py index e3e7f39..2d58cfc 100644 --- a/dometic_cfx3/base.py +++ b/dometic_cfx3/base.py @@ -1,17 +1,2 @@ -""" -dometic_cfx3 base module. -This is the principal module of the dometic_cfx3 project. -here you put your main classes and objects. - -Be creative! do whatever you want! - -If you want to replace this with a Flask application run: - - $ make init - -and then choose `flask` as template. -""" - -# example constant variable NAME = "dometic_cfx3" diff --git a/dometic_cfx3/cli.py b/dometic_cfx3/cli.py index c7ca25d..a2d05ed 100644 --- a/dometic_cfx3/cli.py +++ b/dometic_cfx3/cli.py @@ -1,28 +1,145 @@ -"""CLI interface for dometic_cfx3 project. +import asyncio +import logging -Be creative! do whatever you want! +from dometic_cfx3.client import WifiClient +from dometic_cfx3.topics import DataAction, DataTopic -- Install click or typer and create a CLI app -- Use builtin argparse -- Start a web application -- Import things from your .base module -""" +logging.basicConfig(level=logging.INFO) + + +async def test(): + async with WifiClient("localhost", 13142) as client: + async def callback(topic: DataTopic, value: str): + print(f"{topic}: {value}") + + await client.subscribe(callback) + await client.send(DataAction.PING.value) + # await client.subscribe(DataTopic.SUBSCRIBE_APP_DZ) + print(f"Name: {await client.get_device_name()}") + print(f"compartments: {await client.get_compartment_count()}") + print(f"icemaker: {await client.get_icemaker_count()}") + print(f"door: {await client.get_door_alert()}") + print(f"get_device_name: {await client.get_device_name()}") + # print( + # f"get_communication_alarm: {await client.get_communication_alarm()}" + # ) + # print( + # f"get_temperature_alert_dcm: {await client.get_temperature_alert_dcm()}" + # ) + # print(f"get_station_ssid_0: {await client.get_station_ssid_0()}") + # print( + # f"get_compartment_0_temperature_history_hour: {await client.get_compartment_0_temperature_history_hour()}" + # ) + # print( + # f"get_compartment_1_temperature_history_hour: {await client.get_compartment_1_temperature_history_hour()}" + # ) + # print( + # f"get_compartment_0_temperature_history_day: {await client.get_compartment_0_temperature_history_day()}" + # ) + # print( + # f"get_compartment_1_temperature_history_day: {await client.get_compartment_1_temperature_history_day()}" + # ) + # print( + # f"get_compartment_0_temperature_history_week: {await client.get_compartment_0_temperature_history_week()}" + # ) + # print( + # f"get_compartment_1_temperature_history_week: {await client.get_compartment_1_temperature_history_week()}" + # ) + # print( + # f"get_dc_current_history_hour: {await client.get_dc_current_history_hour()}" + # ) + # print( + # f"get_dc_current_history_day: {await client.get_dc_current_history_day()}" + # ) + # print( + # f"get_dc_current_history_week: {await client.get_dc_current_history_week()}" + # ) + # print(f"get_wifi_ap_connected: {await client.get_wifi_ap_connected()}") + # print( + # f"get_presented_temperature_unit: {await client.get_presented_temperature_unit()}" + # ) + # print( + # f"get_compartment_0_measured_temperature: {await client.get_compartment_0_measured_temperature()}" + # ) + # print( + # f"get_compartment_1_measured_temperature: {await client.get_compartment_1_measured_temperature()}" + # ) + # print( + # f"get_compartment_0_door_open: {await client.get_compartment_0_door_open()}" + # ) + # print( + # f"get_compartment_1_door_open: {await client.get_compartment_1_door_open()}" + # ) + # print(f"get_power_source: {await client.get_power_source()}") + # print( + # f"get_battery_voltage_level: {await client.get_battery_voltage_level()}" + # ) + # print(f"get_cooler_power: {await client.get_cooler_power()}") + # print( + # f"get_compartment_0_power: {await client.get_compartment_0_power()}" + # ) + # print( + # f"get_compartment_1_power: {await client.get_compartment_1_power()}" + # ) + # print( + # f"get_compartment_0_temperature_range: {await client.get_compartment_0_temperature_range()}" + # ) + # print( + # f"get_compartment_1_temperature_range: {await client.get_compartment_1_temperature_range()}" + # ) + # print( + # f"get_compartment_0_set_temperature: {await client.get_compartment_0_set_temperature()}" + # ) + # print( + # f"get_compartment_1_set_temperature: {await client.get_compartment_1_set_temperature()}" + # ) + # print( + # f"get_compartment_0_recommended_range: {await client.get_compartment_0_recommended_range()}" + # ) + # print( + # f"get_compartment_1_recommended_range: {await client.get_compartment_1_recommended_range()}" + # ) + # print( + # f"get_ntc_open_large_error: {await client.get_ntc_open_large_error()}" + # ) + # print( + # f"get_ntc_short_large_error: {await client.get_ntc_short_large_error()}" + # ) + # print( + # f"get_solenoid_valve_error: {await client.get_solenoid_valve_error()}" + # ) + # print( + # f"get_ntc_open_small_error: {await client.get_ntc_open_small_error()}" + # ) + # print( + # f"get_ntc_short_small_error: {await client.get_ntc_short_small_error()}" + # ) + # print( + # f"get_fan_overvoltage_error: {await client.get_fan_overvoltage_error()}" + # ) + # print( + # f"get_compressor_start_fail_error: {await client.get_compressor_start_fail_error()}" + # ) + # print( + # f"get_compressor_speed_error: {await client.get_compressor_speed_error()}" + # ) + # print( + # f"get_controller_over_temperature: {await client.get_controller_over_temperature()}" + # ) + # print(f"get_door_alert: {await client.get_door_alert()}") + # print(f"get_voltage_alert: {await client.get_voltage_alert()}") + # print( + # f"get_battery_protection_level: {await client.get_battery_protection_level()}" + # ) + # print( + # f"get_product_serial_number: {await client.get_product_serial_number()}" + # ) + asyncio.get_event_loop().run_forever() def main(): # pragma: no cover - """ - The main function executes on commands: - `python -m dometic_cfx3` and `$ dometic_cfx3 `. - - This is your program's entry point. - - You can change this function to do whatever you want. - Examples: - * Run a test suite - * Run a server - * Do some other stuff - * Run a command line application (Click, Typer, ArgParse) - * List all available tasks - * Run an application (Flask, FastAPI, Django, etc.) - """ print("This will do something") + asyncio.run(test()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/dometic_cfx3/client.py b/dometic_cfx3/client.py new file mode 100644 index 0000000..4a40df1 --- /dev/null +++ b/dometic_cfx3/client.py @@ -0,0 +1,322 @@ +import asyncio +import json +import logging +from abc import ABC, abstractmethod +from collections import defaultdict +from typing import Any, Optional, List +import socket + +from dometic_cfx3.topics import ( + DataAction, + DataTopic, + DataType, + decode, + encode, + PowerSourceType, + TemperatureUnit, + get_topic, + get_topic_definition, +) + +logger = logging.getLogger(__name__) + + +class Client(ABC): + def __init__(self): + self.callbacks = defaultdict(list) + + async def get_device_name(self) -> Optional[str]: + return await self.get(DataTopic.DEVICE_NAME) + + async def get_compartment_count(self) -> Optional[int]: + return await self.get(DataTopic.COMPARTMENT_COUNT) + + async def get_icemaker_count(self) -> Optional[int]: + return await self.get(DataTopic.ICEMAKER_COUNT) + + async def get_door_alert(self) -> Optional[bool]: + return await self.get(DataTopic.DOOR_ALERT) + + async def get_communication_alarm(self) -> Optional[bool]: + return await self.get(DataTopic.COMMUNICATION_ALARM) + + async def get_temperature_alert_dcm(self) -> Optional[bool]: + return await self.get(DataTopic.TEMPERATURE_ALERT_DCM) + + async def get_station_ssid_0(self) -> Optional[str]: + # starlight + return await self.get(DataTopic.STATION_SSID_0) + + async def get_compartment_0_temperature_history_hour( + self, + ) -> Optional[list[int]]: + # [19.0, 19.0, 19.0, 19.0, 19.4, 20.0, 20.0, 25] + return await self.get(DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_HOUR) + + async def get_compartment_1_temperature_history_hour( + self, + ) -> Optional[list[int]]: + # [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 25] + return await self.get(DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_HOUR) + + async def get_compartment_0_temperature_history_day( + self, + ) -> Optional[list[int]]: + # [17.9, 16.1, 16.4, 17.9, 20.0, 20.7, 18.7, 203] + return await self.get(DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_DAY) + + async def get_compartment_1_temperature_history_day( + self, + ) -> Optional[list[int]]: + # [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 203] + return await self.get(DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_DAY) + + async def get_compartment_0_temperature_history_week( + self, + ) -> Optional[list[int]]: + # [17.1, -3276.8, -3276.8, -3276.8, -3276.8, -3276.8, -3276.8, 247] + return await self.get(DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_WEEK) + + async def get_compartment_1_temperature_history_week( + self, + ) -> Optional[list[int]]: + # [0.0, -3276.8, -3276.8, -3276.8, -3276.8, -3276.8, -3276.8, 247] + return await self.get(DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_WEEK) + + async def get_dc_current_history_hour(self) -> Optional[list[int]]: + # [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 25] + return await self.get(DataTopic.DC_CURRENT_HISTORY_HOUR) + + async def get_dc_current_history_day(self) -> Optional[list[int]]: + # [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 203] + return await self.get(DataTopic.DC_CURRENT_HISTORY_DAY) + + async def get_dc_current_history_week(self) -> Optional[list[int]]: + # [0.0, -3276.8, -3276.8, -3276.8, -3276.8, -3276.8, -3276.8, 247] + return await self.get(DataTopic.DC_CURRENT_HISTORY_WEEK) + + async def get_wifi_ap_connected(self) -> Optional[bool]: + return await self.get(DataTopic.WIFI_AP_CONNECTED) + + async def get_presented_temperature_unit(self) -> Optional[TemperatureUnit]: + # 1 + data = await self.get(DataTopic.PRESENTED_TEMPERATURE_UNIT) + if data: + return TemperatureUnit(data) + + async def get_compartment_0_measured_temperature(self) -> Optional[int]: + # 19.0 + return await self.get(DataTopic.COMPARTMENT_0_MEASURED_TEMPERATURE) + + async def get_compartment_1_measured_temperature(self) -> Optional[int]: + # 0.0 + return await self.get(DataTopic.COMPARTMENT_1_MEASURED_TEMPERATURE) + + async def get_compartment_0_door_open(self) -> Optional[bool]: + return await self.get(DataTopic.COMPARTMENT_0_DOOR_OPEN) + + async def get_compartment_1_door_open(self) -> Optional[bool]: + return await self.get(DataTopic.COMPARTMENT_1_DOOR_OPEN) + + async def get_power_source(self) -> Optional[PowerSourceType]: + # 1 + data = await self.get(DataTopic.POWER_SOURCE) + if data: + return PowerSourceType(data) + + async def get_battery_voltage_level(self) -> Optional[int]: + # 13.1 + return await self.get(DataTopic.BATTERY_VOLTAGE_LEVEL) + + async def get_cooler_power(self) -> Optional[bool]: + return await self.get(DataTopic.COOLER_POWER) + + async def get_compartment_0_power(self) -> Optional[bool]: + return await self.get(DataTopic.COMPARTMENT_0_POWER) + + async def get_compartment_1_power(self) -> Optional[bool]: + return await self.get(DataTopic.COMPARTMENT_1_POWER) + + async def get_compartment_0_temperature_range(self) -> Optional[list[int]]: + # [-22.0, 10.0] + return await self.get(DataTopic.COMPARTMENT_0_TEMPERATURE_RANGE) + + async def get_compartment_1_temperature_range(self) -> Optional[list[int]]: + # [-22.0, 10.0] + return await self.get(DataTopic.COMPARTMENT_1_TEMPERATURE_RANGE) + + async def get_compartment_0_set_temperature(self) -> Optional[int]: + # 0.0 + return await self.get(DataTopic.COMPARTMENT_0_SET_TEMPERATURE) + + async def get_compartment_1_set_temperature(self) -> Optional[int]: + # -15.0 + return await self.get(DataTopic.COMPARTMENT_1_SET_TEMPERATURE) + + async def get_compartment_0_recommended_range(self) -> Optional[list[int]]: + # [-15.0, 4.0] + return await self.get(DataTopic.COMPARTMENT_0_RECOMMENDED_RANGE) + + async def get_compartment_1_recommended_range(self) -> Optional[list[int]]: + # [-15.0, 4.0] + return await self.get(DataTopic.COMPARTMENT_1_RECOMMENDED_RANGE) + + async def get_ntc_open_large_error(self) -> Optional[bool]: + return await self.get(DataTopic.NTC_OPEN_LARGE_ERROR) + + async def get_ntc_short_large_error(self) -> Optional[bool]: + return await self.get(DataTopic.NTC_SHORT_LARGE_ERROR) + + async def get_solenoid_valve_error(self) -> Optional[bool]: + return await self.get(DataTopic.SOLENOID_VALVE_ERROR) + + async def get_ntc_open_small_error(self) -> Optional[bool]: + return await self.get(DataTopic.NTC_OPEN_SMALL_ERROR) + + async def get_ntc_short_small_error(self) -> Optional[bool]: + return await self.get(DataTopic.NTC_SHORT_SMALL_ERROR) + + async def get_fan_overvoltage_error(self) -> Optional[bool]: + return await self.get(DataTopic.FAN_OVERVOLTAGE_ERROR) + + async def get_compressor_start_fail_error(self) -> Optional[bool]: + return await self.get(DataTopic.COMPRESSOR_START_FAIL_ERROR) + + async def get_compressor_speed_error(self) -> Optional[bool]: + return await self.get(DataTopic.COMPRESSOR_SPEED_ERROR) + + async def get_controller_over_temperature(self) -> Optional[bool]: + return await self.get(DataTopic.CONTROLLER_OVER_TEMPERATURE) + + async def get_voltage_alert(self) -> Optional[bool]: + return await self.get(DataTopic.VOLTAGE_ALERT) + + async def get_battery_protection_level(self) -> Optional[int]: + return await self.get(DataTopic.BATTERY_PROTECTION_LEVEL) + + async def get_product_serial_number(self) -> Optional[int]: + return await self.get(DataTopic.PRODUCT_SERIAL_NUMBER) + + @abstractmethod + async def get(self, topic: DataTopic): + pass + + +class WifiClient(Client): + def __init__(self, host: str, port: int) -> None: + self.host = host + self.port = port + self.running = True + self.events = defaultdict(asyncio.Event) + self.device_data = {} + self.send_ack = asyncio.Event() + super().__init__() + + async def __aenter__(self): + + # client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + # client.sendto("DDMD".encode(), (self.host, self.port)) + # data = json.loads(client.recv(1024)) + # self.product_type = data["pid"] + # print(data) + self.product_type = 1 + + self.reader, self.writer = await asyncio.open_connection( + self.host, self.port + ) + asyncio.ensure_future(self.run()) + await self.send(DataAction.PING.value) + return self + + async def subscribe(self, callback, topic: Optional[DataTopic] = None): + if not topic: + self.callbacks[DataTopic.ALL].append(callback) + if self.product_type == 1: + topic = DataTopic.SUBSCRIBE_APP_SZ + elif self.product_type == 2: + topic = DataTopic.SUBSCRIBE_APP_SZI + elif self.product_type == 3: + topic = DataTopic.SUBSCRIBE_APP_DZ + else: + self.callbacks[topic].append(callback) + + logger.debug(f"Subscribe to {topic}") + await self._subscribe(topic) + + async def run(self): + while self.running: + try: + data = await self.reader.readuntil("\r".encode()) + if not data: + break + except asyncio.exceptions.IncompleteReadError: + break + await self.process(data) + + async def publish(self, topic: DataTopic, value): + print("IN HEREEEEEE") + dfn = get_topic_definition(topic) + data = [DataAction.PUBLISH.value] + dfn.param + encode(dfn, value) + await self.send_with_ack(data) + + async def _subscribe(self, topic: DataTopic): + data = [DataAction.SUBSCRIBE.value] + get_topic_definition(topic).param + await self.send_with_ack(data) + + async def send(self, data): + x = json.dumps({"ddmp": data}) + logger.debug(f"Writing {x}") + self.writer.write(f"{x}\r".encode()) + await self.writer.drain() + + async def send_with_ack(self, data): + await self.send(data) + self.send_ack.clear() + await self.send_ack.wait() + + async def get( + self, topic: DataTopic + ): + self.events[topic].clear() + await self._subscribe(topic) + try: + await asyncio.wait_for(self.events[topic].wait(), 1) + except asyncio.TimeoutError: + return + return self.device_data.get(topic) + + async def process(self, data): + msg = json.loads(data)["ddmp"] + logger.debug(f"Received: {msg}") + action = DataAction(msg[0]) + if action == DataAction.PING: + logger.debug("Received PING") + await self.send([DataAction.ACK.value]) + elif action == DataAction.ACK: + logger.debug("Received ACK") + self.send_ack.set() + elif action == DataAction.NAK: + logger.debug("Received NAK") + elif action == DataAction.PUBLISH: + dfn = get_topic(msg[1:5]) + await self.send([DataAction.ACK.value]) + logger.debug(f"Received PUBLISH") + if dfn: + value = None + if len(msg) > 5: + value = msg[5:] + if value: + pdata = decode(dfn, value) + logger.debug(f"Received PUBLISH: {dfn.topic} {pdata}") + self.device_data[dfn.topic] = pdata + self.events[dfn.topic].set() + + await asyncio.gather(*[fn(dfn.topic, pdata) for fn in (self.callbacks[DataTopic.ALL] + self.callbacks[dfn.topic])]) + + elif action == DataAction.SUBSCRIBE: + logger.debug("Received SUBSCRIBE") + await self.send([DataAction.ACK.value]) + + async def __aexit__(self, exc_type, exc_val, exc_tb): + self.writer.close() + await self.writer.wait_closed() diff --git a/dometic_cfx3/haas.py b/dometic_cfx3/haas.py new file mode 100644 index 0000000..190bd30 --- /dev/null +++ b/dometic_cfx3/haas.py @@ -0,0 +1,198 @@ +import asyncio +import logging + +from dometic_cfx3.client import WifiClient +from dometic_cfx3.topics import DataAction, DataTopic +from ha_mqtt_discoverable import Settings, DeviceInfo +from ha_mqtt_discoverable.sensors import Sensor, SensorInfo, BinarySensor, BinarySensorInfo, Switch, SwitchInfo +from paho.mqtt.client import Client, MQTTMessage + +logging.basicConfig(level=logging.DEBUG) + +from queue import Queue + + + +async def test(): + async with WifiClient("localhost", 13142) as client: + # Configure the required parameters for the MQTT broker + mqtt_settings = Settings.MQTT(host="192.168.103.138") + sn = await client.get_product_serial_number() + device_info = DeviceInfo(name=await client.get_device_name(), manufacturer="Dometic", hw_version=sn, identifiers=sn) + + + dt = {} + loop = asyncio.get_event_loop() + command_queue = Queue() + def toggle(_: Client, topic, message: MQTTMessage): + payload = message.payload.decode() + logging.info(f"Received {payload} from HA") + asyncio.run_coroutine_threadsafe(client.publish(topic, True if payload == "ON" else False), loop) + if payload == "ON": + dt.get(topic).on() + else: + dt.get(topic).off() + + + dt[DataTopic.DOOR_ALERT] = BinarySensor(Settings(mqtt=mqtt_settings, entity=BinarySensorInfo(name="door_alert", unique_id="door_alert", device=device_info))) + dt[DataTopic.PRODUCT_SERIAL_NUMBER] = Sensor(Settings(mqtt=mqtt_settings, entity=SensorInfo(name="serial_number", unique_id="serial_number", device=device_info))) + dt[DataTopic.COOLER_POWER] = Switch(Settings(mqtt=mqtt_settings, entity=SwitchInfo(name="cooler_power", unique_id="cooler_power", device=device_info)), toggle, DataTopic.COOLER_POWER) + dt[DataTopic.COMPARTMENT_0_MEASURED_TEMPERATURE] = Sensor(Settings(mqtt=mqtt_settings, entity=SensorInfo(name="compartment_temperature", unique_id="compartment_temperature", unit_of_measurement="°C", device=device_info))) + dt[DataTopic.COMPARTMENT_0_SET_TEMPERATURE] = Sensor(Settings(mqtt=mqtt_settings, entity=SensorInfo(name="compartment_setpoint", unique_id="compartment_setpoint", unit_of_measurement="°C", device=device_info))) + dt[DataTopic.BATTERY_VOLTAGE_LEVEL] = Sensor(Settings(mqtt=mqtt_settings, entity=SensorInfo(name="battery_voltage", unique_id="battery_voltage", unit_of_measurement="V", device=device_info))) + dt[DataTopic.POWER_SOURCE] = Sensor(Settings(mqtt=mqtt_settings, entity=SensorInfo(name="power_source", unique_id="power_source", device=device_info))) + + + + async def callback(topic: DataTopic, value): + if topic in dt: + sensor = dt.get(topic) + print(sensor) + if isinstance(sensor, BinarySensor): + if value: + sensor.on() + else: + sensor.off() + elif isinstance(sensor, Sensor): + if topic == DataTopic.POWER_SOURCE: + value = "dc" if value == 1 else "ac" + sensor.set_state(value) + print(f"{topic}: {value}") + + await client.subscribe(callback) + # for topic, _ in dt.items(): + # await client.subscribe(callback, topic=topic) + + # await client.send(DataAction.PING.value) + # await client.publish(DataTopic.COOLER_POWER, True) + # print(f"Name: {await client.get_device_name()}") + print(f"compartments: {await client.get_compartment_count()}") + # print(f"icemaker: {await client.get_icemaker_count()}") + print(f"door: {await client.get_door_alert()}") + # print(f"get_device_name: {await client.get_device_name()}") + # print( + # f"get_communication_alarm: {await client.get_communication_alarm()}" + # ) + # print( + # f"get_temperature_alert_dcm: {await client.get_temperature_alert_dcm()}" + # ) + # print(f"get_station_ssid_0: {await client.get_station_ssid_0()}") + # print( + # f"get_compartment_0_temperature_history_hour: {await client.get_compartment_0_temperature_history_hour()}" + # ) + # print( + # f"get_compartment_1_temperature_history_hour: {await client.get_compartment_1_temperature_history_hour()}" + # ) + # print( + # f"get_compartment_0_temperature_history_day: {await client.get_compartment_0_temperature_history_day()}" + # ) + # print( + # f"get_compartment_1_temperature_history_day: {await client.get_compartment_1_temperature_history_day()}" + # ) + # print( + # f"get_compartment_0_temperature_history_week: {await client.get_compartment_0_temperature_history_week()}" + # ) + # print( + # f"get_compartment_1_temperature_history_week: {await client.get_compartment_1_temperature_history_week()}" + # ) + # print( + # f"get_dc_current_history_hour: {await client.get_dc_current_history_hour()}" + # ) + # print( + # f"get_dc_current_history_day: {await client.get_dc_current_history_day()}" + # ) + # print( + # f"get_dc_current_history_week: {await client.get_dc_current_history_week()}" + # ) + # print(f"get_wifi_ap_connected: {await client.get_wifi_ap_connected()}") + # print( + # f"get_presented_temperature_unit: {await client.get_presented_temperature_unit()}" + # ) + # print( + # f"get_compartment_0_measured_temperature: {await client.get_compartment_0_measured_temperature()}" + # ) + # print( + # f"get_compartment_1_measured_temperature: {await client.get_compartment_1_measured_temperature()}" + # ) + # print( + # f"get_compartment_0_door_open: {await client.get_compartment_0_door_open()}" + # ) + # print( + # f"get_compartment_1_door_open: {await client.get_compartment_1_door_open()}" + # ) + # print(f"get_power_source: {await client.get_power_source()}") + # print( + # f"get_battery_voltage_level: {await client.get_battery_voltage_level()}" + # ) + # print(f"get_cooler_power: {await client.get_cooler_power()}") + # print( + # f"get_compartment_0_power: {await client.get_compartment_0_power()}" + # ) + # print( + # f"get_compartment_1_power: {await client.get_compartment_1_power()}" + # ) + # print( + # f"get_compartment_0_temperature_range: {await client.get_compartment_0_temperature_range()}" + # ) + # print( + # f"get_compartment_1_temperature_range: {await client.get_compartment_1_temperature_range()}" + # ) + # print( + # f"get_compartment_0_set_temperature: {await client.get_compartment_0_set_temperature()}" + # ) + # print( + # f"get_compartment_1_set_temperature: {await client.get_compartment_1_set_temperature()}" + # ) + # print( + # f"get_compartment_0_recommended_range: {await client.get_compartment_0_recommended_range()}" + # ) + # print( + # f"get_compartment_1_recommended_range: {await client.get_compartment_1_recommended_range()}" + # ) + # print( + # f"get_ntc_open_large_error: {await client.get_ntc_open_large_error()}" + # ) + # print( + # f"get_ntc_short_large_error: {await client.get_ntc_short_large_error()}" + # ) + # print( + # f"get_solenoid_valve_error: {await client.get_solenoid_valve_error()}" + # ) + # print( + # f"get_ntc_open_small_error: {await client.get_ntc_open_small_error()}" + # ) + # print( + # f"get_ntc_short_small_error: {await client.get_ntc_short_small_error()}" + # ) + # print( + # f"get_fan_overvoltage_error: {await client.get_fan_overvoltage_error()}" + # ) + # print( + # f"get_compressor_start_fail_error: {await client.get_compressor_start_fail_error()}" + # ) + # print( + # f"get_compressor_speed_error: {await client.get_compressor_speed_error()}" + # ) + # print( + # f"get_controller_over_temperature: {await client.get_controller_over_temperature()}" + # ) + # print(f"get_door_alert: {await client.get_door_alert()}") + # print(f"get_voltage_alert: {await client.get_voltage_alert()}") + # print( + # f"get_battery_protection_level: {await client.get_battery_protection_level()}" + # ) + # print( + # f"get_product_serial_number: {await client.get_product_serial_number()}" + # ) + while True: + await asyncio.sleep(1) + # topic, value = command_queue.get() + # await client.publish(topic, value) + + +def main(): # pragma: no cover + print("This will do something") + asyncio.run(test()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/dometic_cfx3/topics.py b/dometic_cfx3/topics.py new file mode 100644 index 0000000..fd7fe62 --- /dev/null +++ b/dometic_cfx3/topics.py @@ -0,0 +1,533 @@ +from dataclasses import dataclass +from enum import Enum, auto +import math + +class PowerSourceType(Enum): + AC = 0 + DC = 1 + SOLAR = 2 + +class TemperatureUnit(Enum): + CELCIUS = 0 + FAHRENHEIT = 1 + +class DataAction(Enum): + PUBLISH = 0 + SUBSCRIBE = 1 + PING = 2 + HELLO = 3 + ACK = 4 + NAK = 5 + NOP = 6 + + +class DataType(Enum): + INT16_DECIDEGREE_CELSIUS = auto() + INT8_BOOLEAN = auto() + INT8_NUMBER = auto() + INT16_DECICURRENT_VOLT = auto() + UINT8_NUMBER = auto() + UTF8_STRING = auto() + HISTORY_DATA_ARRAY = auto() + INT16_ARRAY = auto() + EMPTY = auto() + + +class DataTopic(Enum): + ALL = auto() + SUBSCRIBE_APP_SZ = auto() + SUBSCRIBE_APP_SZI = auto() + SUBSCRIBE_APP_DZ = auto() + PRODUCT_SERIAL_NUMBER = auto() + COMPARTMENT_COUNT = auto() + ICEMAKER_COUNT = auto() + COMPARTMENT_0_POWER = auto() + COMPARTMENT_1_POWER = auto() + COMPARTMENT_0_MEASURED_TEMPERATURE = auto() + COMPARTMENT_1_MEASURED_TEMPERATURE = auto() + COMPARTMENT_0_DOOR_OPEN = auto() + COMPARTMENT_1_DOOR_OPEN = auto() + COMPARTMENT_0_SET_TEMPERATURE = auto() + COMPARTMENT_1_SET_TEMPERATURE = auto() + COMPARTMENT_0_RECOMMENDED_RANGE = auto() + COMPARTMENT_1_RECOMMENDED_RANGE = auto() + PRESENTED_TEMPERATURE_UNIT = auto() + COMPARTMENT_0_TEMPERATURE_RANGE = auto() + COMPARTMENT_1_TEMPERATURE_RANGE = auto() + COOLER_POWER = auto() + BATTERY_VOLTAGE_LEVEL = auto() + BATTERY_PROTECTION_LEVEL = auto() + POWER_SOURCE = auto() + ICEMAKER_POWER = auto() + COMMUNICATION_ALARM = auto() + NTC_OPEN_LARGE_ERROR = auto() + NTC_SHORT_LARGE_ERROR = auto() + SOLENOID_VALVE_ERROR = auto() + NTC_OPEN_SMALL_ERROR = auto() + NTC_SHORT_SMALL_ERROR = auto() + FAN_OVERVOLTAGE_ERROR = auto() + COMPRESSOR_START_FAIL_ERROR = auto() + COMPRESSOR_SPEED_ERROR = auto() + CONTROLLER_OVER_TEMPERATURE = auto() + TEMPERATURE_ALERT_DCM = auto() + TEMPERATURE_ALERT_CC = auto() + DOOR_ALERT = auto() + VOLTAGE_ALERT = auto() + DEVICE_NAME = auto() + WIFI_MODE = auto() + BLUETOOTH_MODE = auto() + WIFI_AP_CONNECTED = auto() + STATION_SSID_0 = auto() + STATION_SSID_1 = auto() + STATION_SSID_2 = auto() + STATION_PASSWORD_0 = auto() + STATION_PASSWORD_1 = auto() + STATION_PASSWORD_2 = auto() + STATION_PASSWORD_3 = auto() + STATION_PASSWORD_4 = auto() + CFX_DIRECT_PASSWORD_0 = auto() + CFX_DIRECT_PASSWORD_1 = auto() + CFX_DIRECT_PASSWORD_2 = auto() + CFX_DIRECT_PASSWORD_3 = auto() + CFX_DIRECT_PASSWORD_4 = auto() + COMPARTMENT_0_TEMPERATURE_HISTORY_HOUR = auto() + COMPARTMENT_1_TEMPERATURE_HISTORY_HOUR = auto() + COMPARTMENT_0_TEMPERATURE_HISTORY_DAY = auto() + COMPARTMENT_1_TEMPERATURE_HISTORY_DAY = auto() + COMPARTMENT_0_TEMPERATURE_HISTORY_WEEK = auto() + COMPARTMENT_1_TEMPERATURE_HISTORY_WEEK = auto() + DC_CURRENT_HISTORY_HOUR = auto() + DC_CURRENT_HISTORY_DAY = auto() + DC_CURRENT_HISTORY_WEEK = auto() + + +@dataclass +class TopicDefinition: + """Class for keeping track of an item in inventory.""" + + topic: DataTopic + param: list[int] + data_type: DataType + + +TOPICS = { + # Subscribe All SZ topics + DataTopic.SUBSCRIBE_APP_SZ: TopicDefinition( + topic=DataTopic.SUBSCRIBE_APP_SZ, + param=[1, 0, 0, 129], + data_type=DataType.EMPTY, + ), + # Subscribe All SZ with icemaker topics + DataTopic.SUBSCRIBE_APP_SZI: TopicDefinition( + topic=DataTopic.SUBSCRIBE_APP_SZI, + param=[2, 0, 0, 129], + data_type=DataType.EMPTY, + ), + # Subscribe All DZ + DataTopic.SUBSCRIBE_APP_DZ: TopicDefinition( + topic=DataTopic.SUBSCRIBE_APP_DZ, + param=[3, 0, 0, 129], + data_type=DataType.EMPTY, + ), + # Product Information + DataTopic.PRODUCT_SERIAL_NUMBER: TopicDefinition( + topic=DataTopic.PRODUCT_SERIAL_NUMBER, + param=[0, 193, 0, 0], + data_type=DataType.UTF8_STRING, + ), + # # Properties + DataTopic.COMPARTMENT_COUNT: TopicDefinition( + topic=DataTopic.COMPARTMENT_COUNT, + param=[0, 128, 0, 1], + data_type=DataType.INT8_NUMBER, + ), + DataTopic.ICEMAKER_COUNT: TopicDefinition( + topic=DataTopic.ICEMAKER_COUNT, + param=[0, 129, 0, 1], + data_type=DataType.INT8_NUMBER, + ), + # # Compartment + DataTopic.COMPARTMENT_0_POWER: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_POWER, + param=[0, 0, 1, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.COMPARTMENT_1_POWER: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_POWER, + param=[16, 0, 1, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.COMPARTMENT_0_MEASURED_TEMPERATURE: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_MEASURED_TEMPERATURE, + param=[0, 1, 1, 1], + data_type=DataType.INT16_DECIDEGREE_CELSIUS, + ), + DataTopic.COMPARTMENT_1_MEASURED_TEMPERATURE: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_MEASURED_TEMPERATURE, + param=[16, 1, 1, 1], + data_type=DataType.INT16_DECIDEGREE_CELSIUS, + ), + DataTopic.COMPARTMENT_0_DOOR_OPEN: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_DOOR_OPEN, + param=[0, 8, 1, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.COMPARTMENT_1_DOOR_OPEN: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_DOOR_OPEN, + param=[16, 8, 1, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.COMPARTMENT_0_SET_TEMPERATURE: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_SET_TEMPERATURE, + param=[0, 2, 1, 1], + data_type=DataType.INT16_DECIDEGREE_CELSIUS, + ), + DataTopic.COMPARTMENT_1_SET_TEMPERATURE: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_SET_TEMPERATURE, + param=[16, 2, 1, 1], + data_type=DataType.INT16_DECIDEGREE_CELSIUS, + ), + DataTopic.COMPARTMENT_0_RECOMMENDED_RANGE: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_RECOMMENDED_RANGE, + param=[0, 129, 1, 1], + data_type=DataType.INT16_ARRAY, + ), + DataTopic.COMPARTMENT_1_RECOMMENDED_RANGE: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_RECOMMENDED_RANGE, + param=[16, 129, 1, 1], + data_type=DataType.INT16_ARRAY, + ), + # Presented Temperature Unit + DataTopic.PRESENTED_TEMPERATURE_UNIT: TopicDefinition( + topic=DataTopic.PRESENTED_TEMPERATURE_UNIT, + param=[0, 0, 2, 1], + data_type=DataType.INT8_NUMBER, + ), + DataTopic.COMPARTMENT_0_TEMPERATURE_RANGE: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_TEMPERATURE_RANGE, + param=[0, 128, 1, 1], + data_type=DataType.INT16_ARRAY, + ), + DataTopic.COMPARTMENT_1_TEMPERATURE_RANGE: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_TEMPERATURE_RANGE, + param=[16, 128, 1, 1], + data_type=DataType.INT16_ARRAY, + ), + # Power + DataTopic.COOLER_POWER: TopicDefinition( + topic=DataTopic.COOLER_POWER, + param=[0, 0, 3, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.BATTERY_VOLTAGE_LEVEL: TopicDefinition( + topic=DataTopic.BATTERY_VOLTAGE_LEVEL, + param=[0, 1, 3, 1], + data_type=DataType.INT16_DECICURRENT_VOLT, + ), + DataTopic.BATTERY_PROTECTION_LEVEL: TopicDefinition( + topic=DataTopic.BATTERY_PROTECTION_LEVEL, + param=[0, 2, 3, 1], + data_type=DataType.UINT8_NUMBER, + ), + DataTopic.POWER_SOURCE: TopicDefinition( + topic=DataTopic.POWER_SOURCE, + param=[0, 5, 3, 1], + data_type=DataType.INT8_NUMBER, + ), + DataTopic.ICEMAKER_POWER: TopicDefinition( + topic=DataTopic.ICEMAKER_POWER, + param=[0, 6, 3, 1], + data_type=DataType.INT8_BOOLEAN, + ), + # Errors + DataTopic.COMMUNICATION_ALARM: TopicDefinition( + topic=DataTopic.COMMUNICATION_ALARM, + param=[0, 3, 4, 1], + # data_type=not specified in documentation. Using INT8_BOOLEAN for now + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.NTC_OPEN_LARGE_ERROR: TopicDefinition( + topic=DataTopic.NTC_OPEN_LARGE_ERROR, + param=[0, 1, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.NTC_SHORT_LARGE_ERROR: TopicDefinition( + topic=DataTopic.NTC_SHORT_LARGE_ERROR, + param=[0, 2, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.SOLENOID_VALVE_ERROR: TopicDefinition( + topic=DataTopic.SOLENOID_VALVE_ERROR, + param=[0, 9, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.NTC_OPEN_SMALL_ERROR: TopicDefinition( + topic=DataTopic.NTC_OPEN_SMALL_ERROR, + # Temporary params for mocked broker. Params should be 'param': [0, 11, 4, 1], + param=[0, 17, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.NTC_SHORT_SMALL_ERROR: TopicDefinition( + topic=DataTopic.NTC_SHORT_SMALL_ERROR, + # Temporary params for mocked broker. Params should be 'param': [0, 12, 4, 1], + param=[0, 18, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.FAN_OVERVOLTAGE_ERROR: TopicDefinition( + topic=DataTopic.FAN_OVERVOLTAGE_ERROR, + # Temporary params for mocked broker, Params should be 'param': [0, 32, 4, 1] + param=[0, 50, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.COMPRESSOR_START_FAIL_ERROR: TopicDefinition( + topic=DataTopic.COMPRESSOR_START_FAIL_ERROR, + # Temporary params for mocked broker. Params should be 'param': [0, 33, 4, 1], + param=[0, 51, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.COMPRESSOR_SPEED_ERROR: TopicDefinition( + topic=DataTopic.COMPRESSOR_SPEED_ERROR, + # Temporary params for mocked broker. Params should be 'param': [0, 34, 4, 1], + param=[0, 52, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.CONTROLLER_OVER_TEMPERATURE: TopicDefinition( + topic=DataTopic.CONTROLLER_OVER_TEMPERATURE, + # Temporary params for mocked broker. Params should be 'param': [0, 35, 4, 1], + param=[0, 53, 4, 1], + data_type=DataType.INT8_BOOLEAN, + ), + # Alerts + DataTopic.TEMPERATURE_ALERT_DCM: TopicDefinition( + topic=DataTopic.TEMPERATURE_ALERT_DCM, + param=[0, 3, 5, 1], # used for DCM as source + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.TEMPERATURE_ALERT_CC: TopicDefinition( + topic=DataTopic.TEMPERATURE_ALERT_CC, + param=[0, 0, 5, 1], # used for CC as source + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.DOOR_ALERT: TopicDefinition( + topic=DataTopic.DOOR_ALERT, + param=[0, 1, 5, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.VOLTAGE_ALERT: TopicDefinition( + topic=DataTopic.VOLTAGE_ALERT, + param=[0, 2, 5, 1], + data_type=DataType.INT8_BOOLEAN, + ), + # Communication + DataTopic.DEVICE_NAME: TopicDefinition( + topic=DataTopic.DEVICE_NAME, + param=[0, 0, 6, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.WIFI_MODE: TopicDefinition( + topic=DataTopic.WIFI_MODE, + param=[0, 1, 6, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.BLUETOOTH_MODE: TopicDefinition( + topic=DataTopic.BLUETOOTH_MODE, + param=[0, 3, 6, 1], + data_type=DataType.INT8_BOOLEAN, + ), + DataTopic.WIFI_AP_CONNECTED: TopicDefinition( + topic=DataTopic.WIFI_AP_CONNECTED, + param=[0, 8, 6, 1], + data_type=DataType.INT8_BOOLEAN, + ), + # WiFi Settings + DataTopic.STATION_SSID_0: TopicDefinition( + topic=DataTopic.STATION_SSID_0, + param=[0, 0, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_SSID_1: TopicDefinition( + topic=DataTopic.STATION_SSID_1, + param=[1, 0, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_SSID_2: TopicDefinition( + topic=DataTopic.STATION_SSID_2, + param=[2, 0, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_PASSWORD_0: TopicDefinition( + topic=DataTopic.STATION_PASSWORD_0, + param=[0, 1, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_PASSWORD_1: TopicDefinition( + topic=DataTopic.STATION_PASSWORD_1, + param=[1, 1, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_PASSWORD_2: TopicDefinition( + topic=DataTopic.STATION_PASSWORD_2, + param=[2, 1, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_PASSWORD_3: TopicDefinition( + topic=DataTopic.STATION_PASSWORD_3, + param=[3, 1, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.STATION_PASSWORD_4: TopicDefinition( + topic=DataTopic.STATION_PASSWORD_4, + param=[4, 1, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.CFX_DIRECT_PASSWORD_0: TopicDefinition( + topic=DataTopic.CFX_DIRECT_PASSWORD_0, + param=[0, 2, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.CFX_DIRECT_PASSWORD_1: TopicDefinition( + topic=DataTopic.CFX_DIRECT_PASSWORD_1, + param=[1, 2, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.CFX_DIRECT_PASSWORD_2: TopicDefinition( + topic=DataTopic.CFX_DIRECT_PASSWORD_2, + param=[2, 2, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.CFX_DIRECT_PASSWORD_3: TopicDefinition( + topic=DataTopic.CFX_DIRECT_PASSWORD_3, + param=[3, 2, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.CFX_DIRECT_PASSWORD_4: TopicDefinition( + topic=DataTopic.CFX_DIRECT_PASSWORD_4, + param=[4, 2, 7, 1], + data_type=DataType.UTF8_STRING, + ), + DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_HOUR: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_HOUR, + param=[0, 64, 1, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_HOUR: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_HOUR, + param=[16, 64, 1, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_DAY: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_DAY, + param=[0, 65, 1, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_DAY: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_DAY, + param=[16, 65, 1, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_WEEK: TopicDefinition( + topic=DataTopic.COMPARTMENT_0_TEMPERATURE_HISTORY_WEEK, + param=[0, 66, 1, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_WEEK: TopicDefinition( + topic=DataTopic.COMPARTMENT_1_TEMPERATURE_HISTORY_WEEK, + param=[16, 66, 1, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.DC_CURRENT_HISTORY_HOUR: TopicDefinition( + topic=DataTopic.DC_CURRENT_HISTORY_HOUR, + param=[0, 64, 3, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.DC_CURRENT_HISTORY_DAY: TopicDefinition( + topic=DataTopic.DC_CURRENT_HISTORY_DAY, + param=[0, 65, 3, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), + DataTopic.DC_CURRENT_HISTORY_WEEK: TopicDefinition( + topic=DataTopic.DC_CURRENT_HISTORY_WEEK, + param=[0, 66, 3, 1], + data_type=DataType.HISTORY_DATA_ARRAY, + ), +} + + +def get_topic_definition(topic: DataTopic): + return TOPICS.get(topic) + + +def get_topic(param): + for k, v in TOPICS.items(): + if param == v.param: + return v + + +# export const fetchTopicType = (param) => { +# let ddmDataType; +# Object.keys(ddmTopics).forEach((key) => { +# if (JSON.stringify(ddmTopics[key].PARAM) === JSON.stringify(param)) { +# ddmDataType = ddmTopics[key].TYPE; +# } +# }); +# return ddmDataType; +# }; + + +def bytes_to_celcius(data): + deciDegree = 0 + if data[1] >= 128: + # negative number + deciDegree = ((data[1] * 256) + data[0]) - 65536 + else: + # zero or positive number + deciDegree = (data[1] << 8) | data[0] + return deciDegree / 10 + + +def decode(dfn, data): + if dfn.data_type == DataType.INT16_DECIDEGREE_CELSIUS: + return bytes_to_celcius(data) + elif dfn.data_type == DataType.INT8_BOOLEAN: + return bool(data[0]) + elif dfn.data_type in {DataType.INT8_NUMBER, DataType.UINT8_NUMBER}: + return data[0] + elif dfn.data_type == DataType.INT16_DECICURRENT_VOLT: + current = (data[1] << 8) | data[0] + current = round(current) / 10 + return current + elif dfn.data_type == DataType.UTF8_STRING: + if data: + return bytearray(data).decode() + return "" + elif dfn.data_type == DataType.HISTORY_DATA_ARRAY: + return [ + bytes_to_celcius(data[0:2]), + bytes_to_celcius(data[2:4]), + bytes_to_celcius(data[4:6]), + bytes_to_celcius(data[6:8]), + bytes_to_celcius(data[8:10]), + bytes_to_celcius(data[10:12]), + bytes_to_celcius(data[12:14]), + data[14], + ] + elif dfn.data_type == DataType.INT16_ARRAY: + return [bytes_to_celcius(data[0:2]), bytes_to_celcius(data[2:4])] + elif dfn.data_type == DataType.EMPTY: + return data + return data + + +def encode(dfn, value): + if dfn.data_type == DataType.INT16_DECIDEGREE_CELSIUS: + deciDegree = math.ceil(value * 10) + c1 = deciDegree & 0xff + c2 = (deciDegree >> 8) & 0xff + return [c1, c2] + elif dfn.data_type == DataType.INT8_BOOLEAN: + return [1] if value else [0] + elif dfn.data_type in {DataType.INT8_NUMBER, DataType.UINT8_NUMBER}: + return [value] + elif dfn.data_type == DataType.UTF8_STRING: + raise NotImplemented() + raise NotImplemented() \ No newline at end of file