denpamusic / PyPlumIO

PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
MIT License
14 stars 2 forks source link

Unable to get/set schedule. #18

Open jszkiela72 opened 9 months ago

jszkiela72 commented 9 months ago

Is there an existing issue for this?

I'm having the following issue:

  1. When I run the "Plum ecoMAX: get_schedule" service for heating, the integration returns all values as "day". This is not correct because in the controller I have it set from 00.00 to 06.00 "night" from 6.00 to 15.00 "day" from 15.00 to 00.00 "night" " I can't save the schedule to the controller using the Plum ecoMAX service: set_schedule" for heating.

  2. When I run the "Plum ecoMAX: get_schedule" service for water heater, the integration returns the correct values. But I can't save the schedule to the controller using the Plum ecoMAX service: set_schedule" for the water heater.

I have following devices connected:

I'm connecting to my devices using:

Ethernet/WiFi to RS-485 converter

I'm seeing following log messages:

No response

My diagnostics data:

config_entry-plum_ecomax-3d173fc70d439aca63ce86c959ccc479.json.txt

Code of Conduct

denpamusic commented 9 months ago

From your diagnostics the issue is not related to the integration, but to the PyPlumIO library, which handles low-level protocol.

From a few cues, such as incorrect fuel level, missing model string and partially working schedules, it seems that Plum changed structure of related frames for your model. As PyPlumIO was initially writen for ecoMAX 850P in 2022, changes in firmware done by Plum in further models/firmwares can break the library's ability to decode frames.

This prompts future reverse-engineering of what changed in your device firmware. I'll get to it on the weekends, hovewer it will most likely take more than couple days. Sorry for inconvenience!

jszkiela72 commented 9 months ago

I confirm that my controller is ecoMAX 860P6-O. If I can help you with anything, write please.

Regards Jarek

denpamusic commented 9 months ago

Just so that I have somewhere to start, could you please elaborate if there any errors, when trying to set water heater schedule using set_schedule service or it just silently fails?

jszkiela72 commented 9 months ago

Hi

There are no errors after calling the service. There are no errors in the HA logs.

Regards, Jarek

śr., 13 gru 2023 o 02:37 Denis Paavilainen @.***> napisał(a):

Just so that I have somewhere to start, could you please elaborate if there any errors, when trying to set water heater schedule using set_schedule service or it just silently fails?

— Reply to this email directly, view it on GitHub https://github.com/denpamusic/PyPlumIO/issues/18#issuecomment-1853128287, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR7UDQNAKMUBL2DV5N34TVDYJEBFJAVCNFSM6AAAAABAK37CP2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNJTGEZDQMRYG4 . You are receiving this because you authored the thread.Message ID: @.***>

denpamusic commented 9 months ago

This is not good :(

If you still want to help and know how to run arbitrary python scripts, could you please run one below and send me schedules.json file that it'll produce.

If you don't know how to run python scripts or not comfortable with doing so, don't worry, I'll still try to solve this issue, it's just gonna take more time.

If you decided to help, this simple heavily-commented script will simply send a request for schedules to your boiler and once it'll get a response, it'll save raw bytes and decoded data into JSON file. This way I can obtain raw bytes of the schedule response from your device and try to deduce why it is decoded incorrectly.

Don't forget to install PyPlumIO via pip before running this script:

$ pip install pyplumio

and set HOST and PORT variables to point to your converter.

"""Collect a single frame and dumps it to the file."""

import asyncio
import json
import sys

import pyplumio
from pyplumio.const import DeviceType
from pyplumio.frames import requests, responses
from pyplumio.helpers.parameter import ParameterValues

# Set host and port here.
HOST = "localhost"
PORT = 8899

# ------------------------------------- #
# Do not edit anything below this line. #
# ------------------------------------- #
FILENAME = "schedules.json"

def encode_parameter_values(o):
    """Encode ParameterValues to JSON."""
    if isinstance(o, ParameterValues):
        o = {
            "value": o.value,
            "min_value": o.min_value,
            "max_value": o.max_value,
        }

    return o

async def main() -> int:
    """Collect a single schedule response frame and dump it to the JSON file.

    We'll use DummyProtocol here, since we'll need direct control over
    connection and access to a raw ecoMAX frame.
    """
    async with pyplumio.open_tcp_connection(
        host=HOST, port=PORT, protocol=pyplumio.DummyProtocol()
    ) as connection:
        print("Requesting schedules from the ecoMAX...")
        await connection.writer.write(
            requests.SchedulesRequest(recipient=DeviceType.ECOMAX)
        )

        print("Waiting for response...")
        while connection.connected:
            if isinstance(
                (response := await connection.reader.read()),
                responses.SchedulesResponse,
            ):
                print(f"Got response ({response.length} bytes): {response.hex()}")
                break

        print(f"Saving frame to {FILENAME}...")
        with open(FILENAME, "w", encoding="UTF-8") as file:
            # We'll open a file and save dictionary containing
            # our frame's message represented as hex string and
            # decoded frame data.
            file.write(
                json.dumps(
                    {
                        "message": response.hex(),
                        "data": response.data,
                    },
                    default=encode_parameter_values,
                    indent=2,
                )
            )

        print("All done!")

sys.exit(asyncio.run(main()))
jszkiela72 commented 9 months ago

Hi

It sends you the collected data. Are they helpful to you?

Regards, Jarek

czw., 14 gru 2023 o 03:19 Denis Paavilainen @.***> napisał(a):

This is not good :(

If you still want to help and know how to run arbitrary python scripts, could you please run one below and send me schedules.json file that it'll produce.

If you don't know how to run python scripts or not comfortable with doing so, don't worry, I'll still try to solve this issue, it's just gonna take more time.

If you decided to help, this simple heavily-commented script will simply send a request for schedules to your boiler and once it'll get a response, it'll save raw bytes and decoded data into JSON file. This way I can obtain raw bytes of the schedule response from your device and try to deduce why it is decoded incorrectly.

Don't forget to install PyPlumIO via pip before running this script:

$ pip install pyplumio

and set HOST and PORT variables to point to your converter.

"""Collect a single frame and dumps it to the file.""" import asyncioimport jsonimport sys import pyplumiofrom pyplumio.const import DeviceTypefrom pyplumio.frames import requests, responsesfrom pyplumio.helpers.parameter import ParameterValues

Set host and port here.HOST = "localhost"PORT = 8899

------------------------------------- ## Do not edit anything below this line. ## ------------------------------------- #FILENAME = "schedules.json"

def encode_parameter_values(o): """Encode ParameterValues to JSON.""" if isinstance(o, ParameterValues): o = { "value": o.value, "min_value": o.min_value, "max_value": o.max_value, }

return o

async def main() -> int: """Collect a single schedule response frame and dump it to the JSON file. We'll use DummyProtocol here, since we'll need direct control over connection and access to a raw ecoMAX frame. """ async with pyplumio.open_tcp_connection( host=HOST, port=PORT, protocol=pyplumio.DummyProtocol() ) as connection: print("Requesting schedules from the ecoMAX...") await connection.writer.write( requests.SchedulesRequest(recipient=DeviceType.ECOMAX) )

    while connection.connected:
        print("Waiting for response...")
        if isinstance(
            (response := await connection.reader.read()),
            responses.SchedulesResponse,
        ):
            print(f"Got response ({response.length} bytes): {response.hex()}")
            break

    print(f"Saving frame to {FILENAME}...")
    with open(FILENAME, "w", encoding="UTF-8") as file:
        # We'll open a file and save dictionary containing
        # our frame's message represented as hex string and
        # decoded frame data.
        file.write(
            json.dumps(
                {
                    "message": response.hex(),
                    "data": response.data,
                },
                default=encode_parameter_values,
                indent=2,
            )
        )

    print("All done!")

sys.exit(asyncio.run(main()))

— Reply to this email directly, view it on GitHub https://github.com/denpamusic/PyPlumIO/issues/18#issuecomment-1855004032, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR7UDQJPKSBZQB5EXE3DGVLYJJO3BAVCNFSM6AAAAABAK37CP2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNJVGAYDIMBTGI . You are receiving this because you authored the thread.Message ID: @.***>

denpamusic commented 9 months ago

It doesn't send it to me, it simply writes it to the file named schedules.json next to the script itself. If you managed to run the script, please send me this file yourself, it will be very helpful, since I'll then have an undecoded bytes from your device and can then implement changes into decoder and test it right away using this data.

jszkiela72 commented 9 months ago

schedules.json

denpamusic commented 9 months ago

Thank you very much for your help! I'll plug this data into a test case and try to rewrite decoder until it matches this for heating schedule:

This is not correct because in the controller I have it set from 00.00 to 06.00 "night" from 6.00 to 15.00 "day" from 15.00 to 00.00 "night" "

jszkiela72 commented 9 months ago

Hi,

Today I had more time to look at the schedules. I have to apologize to you because I misled you earlier. I will divide my explanation into two topics.

  1. DHW schedule.

I once wrote to you that I can read the DHW schedule, but I cannot set it. It turned out that I could set it up, but I was doing it incorrectly. This is because, before I installed the integration, I had set a schedule on my room panel (ecoMAX 860P Touch) for: 00.00-06.00 night (means do not heat the water), 06.00-15.00 day (heat water), 15.00-24.00 night. Then I started the service and set, for example, 12.00-14.00 day. There was no change on the room panel so I thought the service was not working properly. Now I know that to set water heating between 12.00-14.00 I should call the service 06.00-11.30 night and the second service 14.00-15.00 night. Alternatively 00.00:23.30 night and then 12.00-13.30 day. I don't know how to do it in Lovelace yet - maybe you can tell me something ;)

  1. CH schedule.

As I wrote earlier here, I could neither read nor save the schedule. Today I noticed that Plum ecoMAX 860P6-O + Plum ecoMAX 860P6-O TOUCH has two schedules for CH. One is in the pellet boiler and the other in the room panel. They are independent and their settings do not overlap. I checked that I use the service to set the schedule in the pellet boiler, but I don't use it (it's turned off), I use the one in the room panel. Tell me, are you able to improve the integration so that it allows you to change the schedule in the room panel?

If something is not clear to you, do not hesitate to ask.

Regards, Jarek

denpamusic commented 9 months ago

Hi!

Thank you for the clarification!

  1. It's ok. Schedule services documentation are quite bad and need improvement, so it's no wonder you've got confused. Furthermore there's no easy way to use schedules with lovelace, because such card doesn't exist. The best possible way for now would probably to create separate datetime helpers for start of night mode and start of day mode and create a script that will parse those inputs and send it to the set_schedule service. I could try to create a HASS blueprint for such use case, I wanted to learn how to use them anyways.

  2. As for heating schedule, you are right, PyPlumIO only knows how to handle ecoMAX schedules. ecoSTER panel schedules is requested with different request frame REQUEST_GET_ECOSTER_SCHEDULES[frame_type=88] which is unsupported by PyPlumIO yet, but shouldn't be very hard to implement since frame format should be literally the same as ecoMAX schedules. Could you please run another script for me. This one should request ecoSTER panel schedules and write them to schedules.json file same as before. This script is a bit larger, since we're trying to get a frame not yet supported by the library. Don't forget to change HOST and PORT variables.

"""Collect a ecoSTER schedules frame and dumps it to the file."""

import asyncio
import json
import sys
from typing import ClassVar

import pyplumio
from pyplumio import ChecksumError
from pyplumio.const import DeviceType
from pyplumio.frames import Request, Response, bcc
from pyplumio.helpers.parameter import ParameterValues
from pyplumio.stream import FrameReader, FrameWriter, struct_header

# Set host and port here.
HOST = "localhost"
PORT = 8899

# ------------------------------------- #
# Do not edit anything below this line. #
# ------------------------------------- #
FILENAME = "schedules.json"

class CustomFrameType:
    """Extend frame type enum with ecoSTER schedule frames."""

    REQUEST_ECOSTER_SCHEDULES = 88
    RESPONSE_ECOSTER_SCHEDULES = 216

class CustomRequest(Request):
    """Custom request frame."""

    __slots__ = ("frame_type",)

    frame_type: ClassVar[CustomFrameType | int]

class CustomResponse(Response):
    """Custom response frame."""

    __slots__ = ("frame_type",)

    frame_type: ClassVar[CustomFrameType | int]

class CustomFrameReader(FrameReader):
    """Custom frame reader that allows unknown frames."""

    async def read(self) -> CustomResponse | None:
        """Read any frames from the stream.."""
        (
            header,
            length,
            recipient,
            sender,
            sender_type,
            econet_version,
        ) = await self._read_header()

        if recipient not in (DeviceType.ECONET, DeviceType.ALL):
            return None

        payload = await self._reader.readexactly(length - struct_header.size)

        if payload[-2] != bcc(header + payload[:-2]):
            raise ChecksumError(f"Incorrect frame checksum ({payload[-2]})")

        response = CustomResponse(
            recipient=recipient,
            message=payload[1:-2],
            sender=sender,
            sender_type=sender_type,
            econet_version=econet_version,
        )
        response.frame_type = payload[0]

        return response

class CustomDummyProtocol(pyplumio.DummyProtocol):
    """A dummy protocol with a custom reader that accepts unknown frames."""

    def connection_established(
        self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
    ) -> None:
        """Set reader and writer attributes and set the connected event."""
        self.reader = CustomFrameReader(reader)
        self.writer = FrameWriter(writer)
        self.connected.set()

def encode_parameter_values(o):
    """Encode ParameterValues to JSON."""
    if isinstance(o, ParameterValues):
        o = {
            "value": o.value,
            "min_value": o.min_value,
            "max_value": o.max_value,
        }

    return o

async def main() -> int:
    """Collect a single schedule response frame and dump it to the JSON file.

    We'll use DummyProtocol here, since we'll need direct control over
    connection and access to a raw ecoMAX frame.
    """
    async with pyplumio.open_tcp_connection(
        host=HOST, port=PORT, protocol=CustomDummyProtocol()
    ) as connection:
        print("Requesting ecoSTER schedules from the ecoMAX...")
        request = CustomRequest(recipient=DeviceType.ECOMAX)
        request.frame_type = CustomFrameType.REQUEST_ECOSTER_SCHEDULES
        await connection.writer.write(request)

        print("Waiting for response...")
        while connection.connected:
            response: CustomResponse = await connection.reader.read()
            if (
                response is not None
                and response.frame_type == CustomFrameType.RESPONSE_ECOSTER_SCHEDULES
            ):
                print(f"Got response ({response.length} bytes): {response.hex()}")
                break

        print(f"Saving frame to {FILENAME}...")
        with open(FILENAME, "w", encoding="UTF-8") as file:
            # We'll open a file and save dictionary containing
            # our frame's message represented as hex string and
            # decoded frame data.
            file.write(
                json.dumps(
                    {
                        "message": response.hex(),
                        "data": response.data,
                    },
                    default=encode_parameter_values,
                    indent=2,
                )
            )

        print("All done!")

sys.exit(asyncio.run(main()))
jszkiela72 commented 9 months ago

Hi

There is some problem with this script.

When I run it, in the terminal I have:

Requesting ecoSTER schedules form tne ecoMAX... wait for response...

The script doesn't end. On the RS485 to ethernet converter page, I see that the script is trying to send data, but ecoMAX returns only 10 bytes of data.

I checked, the previous script executes correctly, so it's not a hardware fault.

Regards, Jarek

denpamusic commented 9 months ago

It's means that ecoMAX didn't recognize a request and didn't respond to it. Let me check if there are any additional request body requirements...

edit. Found it. It seems like we'll need to send this request to the panel address instead.

Try now. This time we'll request schedules from the panel.

If it scripts hang on waiting for response you can cancel it by pressing Ctrl + C. Sorry for not mentioning it before.

"""Collect a single frame and dumps it to the file."""

import asyncio
import json
import sys
from typing import ClassVar

import pyplumio
from pyplumio import ChecksumError
from pyplumio.const import DeviceType
from pyplumio.frames import Request, Response, bcc
from pyplumio.helpers.parameter import ParameterValues
from pyplumio.stream import FrameReader, FrameWriter, struct_header

# Set host and port here.
HOST = "localhost"
PORT = 8899

# ------------------------------------- #
# Do not edit anything below this line. #
# ------------------------------------- #
FILENAME = "schedules.json"

class CustomFrameType:
    """Extend frame type enum with ecoSTER schedule frames."""

    REQUEST_ECOSTER_SCHEDULES = 88
    RESPONSE_ECOSTER_SCHEDULES = 216

class CustomRequest(Request):
    """Custom request frame."""

    __slots__ = ("frame_type",)

    frame_type: ClassVar[CustomFrameType | int]

class CustomResponse(Response):
    """Custom response frame."""

    __slots__ = ("frame_type",)

    frame_type: ClassVar[CustomFrameType | int]

class CustomFrameReader(FrameReader):
    """Custom frame reader that allows unknown frames."""

    async def read(self) -> CustomResponse | None:
        """Read any frames from the stream.."""
        (
            header,
            length,
            recipient,
            sender,
            sender_type,
            econet_version,
        ) = await self._read_header()

        if recipient not in (DeviceType.ECONET, DeviceType.ALL):
            return None

        payload = await self._reader.readexactly(length - struct_header.size)

        if payload[-2] != bcc(header + payload[:-2]):
            raise ChecksumError(f"Incorrect frame checksum ({payload[-2]})")

        response = CustomResponse(
            recipient=recipient,
            message=payload[1:-2],
            sender=sender,
            sender_type=sender_type,
            econet_version=econet_version,
        )
        response.frame_type = payload[0]

        return response

class CustomDummyProtocol(pyplumio.DummyProtocol):
    """A dummy protocol with a custom reader that accepts unknown frames."""

    def connection_established(
        self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
    ) -> None:
        """Set reader and writer attributes and set the connected event."""
        self.reader = CustomFrameReader(reader)
        self.writer = FrameWriter(writer)
        self.connected.set()

def encode_parameter_values(o):
    """Encode ParameterValues to JSON."""
    if isinstance(o, ParameterValues):
        o = {
            "value": o.value,
            "min_value": o.min_value,
            "max_value": o.max_value,
        }

    return o

async def main() -> int:
    """Collect a single schedule response frame and dump it to the JSON file.

    We'll use DummyProtocol here, since we'll need direct control over
    connection and access to a raw ecoMAX frame.
    """
    async with pyplumio.open_tcp_connection(
        host=HOST, port=PORT, protocol=CustomDummyProtocol()
    ) as connection:
        print("Requesting schedules from the ecoSTER...")
        request = CustomRequest(recipient=DeviceType.ECOSTER)
        request.frame_type = CustomFrameType.REQUEST_ECOSTER_SCHEDULES
        await connection.writer.write(request)

        print("Waiting for response...")
        while connection.connected:
            response: CustomResponse = await connection.reader.read()
            if (
                response is not None
                and response.frame_type == CustomFrameType.RESPONSE_ECOSTER_SCHEDULES
            ):
                print(f"Got response ({response.length} bytes): {response.hex()}")
                break

        print(f"Saving frame to {FILENAME}...")
        with open(FILENAME, "w", encoding="UTF-8") as file:
            # We'll open a file and save dictionary containing
            # our frame's message represented as hex string and
            # decoded frame data.
            file.write(
                json.dumps(
                    {
                        "message": response.hex(),
                        "data": response.data,
                    },
                    default=encode_parameter_values,
                    indent=2,
                )
            )

        print("All done!")

sys.exit(asyncio.run(main()))
jszkiela72 commented 9 months ago

Now it's ok. I see that the script collected very little data, but maybe this is what you need.

Regards, Jarek schedules.json

denpamusic commented 9 months ago

Yes, thank you very much! This is exactly what I need.

The collected data seemed short to you, because unlike previous script, it lacks decoded part, since PyPlumIO doesn't know how to decode this kind of message yet. But most important part, which is the message itself is there:

frame_start = 0x68
frame_length = 0x3C00 (60 bytes)
recipient = 0x00 (DeviceType.ALL)
sender = 0x51 (DeviceType.ECOSTER)
sender_type = 0x3C
version = 0x00
frame_type = 0xD8 (RESPONSE_THERMOSTAT_SCHEDULES)
schedule_data= 0x1001011001FDFDFD000000000000000FFFFC0000000FFFFC0000000FFFFC0000000FFFFC0000000FFFFC0000000000000000
crc = 0x11
frame_end = 16

I'll need to think about how to best integrate this with HASS, since we now have separate schedule requests for two different devices, but I'll get it done until integration version 0.4.2.

Thanks again for your help! It's greatly appreciated!

jszkiela72 commented 9 months ago

Ok. I will wait for the development of integration. If you need additional data, do not hesitate to write.

jszkiela72 commented 6 months ago

Hello, I see that you have released version 0.42 of integration in which you have separated ecoSTER as a separate device. I updated the integration but I still don't see the option to change schedules on ecoSTER. Am I doing something wrong or have you not added this functionality yet?

Regards, Jarek

denpamusic commented 6 months ago

Hi,

ecoSTER schedules are not part of current minor release.

Development timeline got a bit mangled, due to me having very little time to work on the project during almost all of January and early February. Many small fixes and changes are accumulated during the time, so it made more sense to release them as separate minor version. I try not include many bugfixes and large features in the same release together, since large new features tend to cause issues for some people, and then they are forced to choose between downgrading and having old unfixed bugs or upgrading and getting new ones, introduced by the new features.

I'll revisit ecoSTER schedules once custom entities PR are done and close this issue, when schedules are implemented.

Sorry for the delay.