Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposure as Transport #15

Closed
smar000 opened this issue Feb 5, 2021 · 95 comments
Closed

Exposure as Transport #15

smar000 opened this issue Feb 5, 2021 · 95 comments

Comments

@smar000
Copy link

smar000 commented Feb 5, 2021

Hi @zxdavb

Not an issue, but I'm the author of evogateway/listener - you may recall we had a brief chat a few months back in the automatedhome forum. I was wondering if you've had any further thoughts on exposing your library as a transport. No urgency!

Many thanks.

@zxdavb
Copy link
Owner

zxdavb commented Feb 5, 2021

Hi @smar000

It is fully exposed as a transport, using the standard asyncio Protocol/Transport model.

Have a look at client.py, or - more simply:

from evohome_rf import Gateway

def process_message(msg) -> None:
    # e.g. msg.payload is the the message dict, msg.src.id is the source device address
    dtm = f"{msg.dtm:%H:%M:%S.%f}"[:-3]
    print(f"{dtm} {msg}")

async main(serial_port, **kwargs):
    gwy = Gateway(serial_port, **kwargs)
    protocol, _ = gwy.create_client(process_message)

    await asyncio.create_task(gwy.start())

if __name__ == "__main__":
    asyncio.run(main("/dev/ttyUSB0"))

The idea is - as I'm sure you'll now - is to add your evohome_rf --> RESTful gateway in process_message().

Receiving messages (an inbound msg) is pretty good, but sending commands (an outbound msg) isn't fully fleshed out yet - you can use gwy.send_data("RQ ...") for now.

There are a shedload of config parameters available (see lib_kwargs in client.py and also schema.py) - reduce_processing and the allow/block lists are a good start, I guess.

Workflow is a bit broken, presently: I'll tidy up the master branch today/tomorrow & move dev to another branch.

Any questions, just ask!

@smar000
Copy link
Author

smar000 commented Feb 5, 2021

Thanks for the quick response back, and that's great. I haven't actually had a chance to look at your latest code but will hopefully do so over the weekend.

Thanks again and I'm sure I will have questions... :)

@zxdavb
Copy link
Owner

zxdavb commented Feb 5, 2021

No worries: I'd be very happy to see someone else using it!

@smar000
Copy link
Author

smar000 commented Feb 6, 2021

You have clearly done a tremendous amount of work - really impressive!!

One small issue - I am getting the following error/stack trace:

Traceback (most recent call last):
  File "./evohome2mqtt.py", line 65, in <module>
    asyncio.run(main("/dev/evoftdi"))
  File "/usr/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/usr/lib/python3.7/asyncio/base_events.py", line 579, in run_until_complete
    return future.result()
  File "./evohome2mqtt.py", line 59, in main
    await asyncio.create_task(gwy.start())
  File "/opt/scripts/evohome/evohome_rf/evohome_rf/__init__.py", line 157, in start
    serial_port=self.serial_port,
  File "/opt/scripts/evohome/evohome_rf/evohome_rf/transport.py", line 720, in create_pkt_stack
    ser_instance.set_low_latency_mode(True)  # only for FTDI?
AttributeError: 'Serial' object has no attribute 'set_low_latency_mode'

Commenting out the respective lines in transport.py allows me to continue, without any noticeable issues. I am using a couple of nanoCUL devices. pyserial-asyncio is version 0.5.

@zxdavb
Copy link
Owner

zxdavb commented Feb 6, 2021

Thanks - I will fix this - it seems your nanoCUL is not FTDI?

I had a look at your code - this library will do a lot of the heavy lifting for you - designed exactly for your use-case. Good luck!

Hmmm. Should be:

    if os.name == "posix":
        try:
            ser_instance.set_low_latency_mode(True)  # only for FTDI?
        except AttributeError:  # TODO: also: ValueError?
            pass

Anyway, removing that code block will do no harm.

@smar000
Copy link
Author

smar000 commented Feb 6, 2021

Yes absolutely - I should be able shrink my code down signficantly.

WRT the latency issue, I tried with 2 nanoCULs. One with an FTDI and one with CH340. Both gave the same issue, so I'm not entirely sure that it is FTDI related.

Anyway, its running without the lines, and so not a big issue!

@smar000
Copy link
Author

smar000 commented Feb 8, 2021

Hi @zxdavb

I have got a skeleton version of my system working with your library - looking great so far. Would you mind giving me a full sample scheme showing controller, dhw, ufh controller, trvs, zones etc (I had a look at schema.py but it would be quicker if I had a sample to look at), but haven't been able to get device names to show correctly.

Also are you open to exposing some further properties/attributes, e.g. in Message object, in the __repr__ function, you have some useful bits that could be re-used, e.g, src, dst, code_name, payload from the code below (appropropriately renamed so as not to clash with the main class properties of the same name):

        if self.src.id == self.devs[0].id:
            src = display_name(self.src)
            dst = display_name(self.dst) if self.dst is not self.src else ""
        else:
            src = ""
            dst = display_name(self.src)

        code_name = CODE_NAMES.get(self.code, f"unknown_{self.code}")
        payload = self.raw_payload if self.len < 4 else f"{self.raw_payload[:5]}..."[:9]

Thanks!

@martynwendon
Copy link

I have got a skeleton version of my system working with your library - looking great so far

@smar000 apologies for barging in on the thread, but, I was literally just looking at seeing if evohome_rf could be used for a project to integrate directly with MQTT so your question is great timing.

Currently using an HGI80 on a really old and heavily customised Domoticz install just for evohome. I've been considering migrating to Home Assistant but my whole system is modular built using node-red & MQTT, so while moving to HA would get me an up-to-date evohome integration I'm trying to move away from using a myriad of different smart home software just to provide specific integrations. I'd much rather have hardware <-> MQTT directly!

Is your evohome2mqtt project public anywhere? Happy to help with testing in return for access, I've got a spare HGI80 that I used for development in the past so would run with that so as not to impact my live system.

@smar000
Copy link
Author

smar000 commented Feb 8, 2021

Hi @martynwendon I have an existing evohome -> MQTT system in my repo (evolistener/gateway), where I rewrote a lot of the domoticz packet decoding/encoding to python. That system is working reasonably well.

Having looked at @zxdavb work here though, he has a built a much more robust - and comprehensive - packet decoding/encoding library, and so it makes sense for me to migrate over to his library.

My current 'skeleton' system is no where near ready for public use, and will be many weeks/month or so away before I upload it publicly.

@smar000
Copy link
Author

smar000 commented Feb 8, 2021

@zxdavb I came across an interesting error a short while ago:

16:17:12.784 000 RQ --- 01:191706 13:102710 --:------ 0008 001 00 < Validation error: Invalid verb/code for 01:191706: RQ/0008
16:17:12.786 044 RP --- 13:102710 01:191706 --:------ 0008 002 00C8 < CorruptStateError
16:17:12.786 handle_exception(): Caught: Exception in callback SerialTransport._read_ready()
16:17:12.786 Unhandled error in exception handler
context: {'message': 'Exception in callback SerialTransport._read_ready()', 'exception': CorruptStateError(CorruptStateError(...), '13:102710 (BDR) has changed controller: 01:139901 to 01:191706'), 'handle': <Handle SerialTransport._read_ready()>}
Traceback (most recent call last):
  File "/usr/lib/python3.7/asyncio/base_events.py", line 1639, in call_exception_handler
    self._exception_handler(self, context)
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/__init__.py", line 124, in handle_exception
    raise exc
  File "/usr/lib/python3.7/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
  File "/usr/local/lib/python3.7/dist-packages/serial_asyncio/__init__.py", line 106, in _read_ready
    self._protocol.data_received(data)
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/transport.py", line 408, in data_received
    self._data_received(*create_pkt(line))
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/transport.py", line 612, in _data_received
    self._callback(pkt)  # only wanted PKTs up to the MSG transport's handler
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/protocol.py", line 139, in _pkt_receiver
    [p.data_received(msg) for p in self._protocols]
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/protocol.py", line 139, in <listcomp>
    [p.data_received(msg) for p in self._protocols]
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/protocol.py", line 368, in data_received
    self._callback(msg)
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/message.py", line 550, in process_msg
    create_devices(msg)  # from pkt header & from msg payload (e.g. 000C)
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/message.py", line 378, in create_devices
    this._gwy._get_device(this.src, ctl_addr=this.dst)
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/__init__.py", line 234, in _get_device
    device._set_ctl(ctl)
  File "/usr/local/lib/python3.7/dist-packages/evohome_rf-0.5.5-py3.7.egg/evohome_rf/devices.py", line 201, in _set_ctl
    f"{self} has changed controller: {self._ctl.id} to {ctl.id}"
evohome_rf.exceptions.CorruptStateError: The system state is inconsistent: 13:102710 (BDR) has changed controller: 01:139901 to 01:191706 (try restarting the client library)

I certainly haven't changed controllers, and don't recognise 01:191706 as one of my devices... I don't believe any of my neighbours have an evohome system either.

Any ideas?

Thanks.

@martynwendon
Copy link

Hi @martynwendon I have an existing evohome -> MQTT system in my repo (evolistener/gateway), where I rewrote a lot of the domoticz packet decoding/encoding to python. That system is working reasonably well.

I think I did try this a few years ago but didn't have much success with the HGI80. I don't recall what the problem was, but I didn't have a spare HGI80 back then so didn't spend too much time on it. I stayed with Domoticz as that was already working.

I'll take another look though since I have the spare HGI80 now so can afford to play around a bit more.

Having looked at @zxdavb work here though, he has a built a much more robust - and comprehensive - packet decoding/encoding library, and so it makes sense for me to migrate over to his library.

Agreed, the progress has been amazing and I've been tracking the thread on the Home Assistant forum for quite some time. I had a bit of spare time so thought I'd dig into possibly writing my own MQTT direct gateway using the evohome_rf library, hence I spotted your question.

My current 'skeleton' system is no where near ready for public use, and will be many weeks/month or so away before I upload it publicly.

No problem at all, I'll check back at a later date. In the meantime if you do want anybody to test please let me know.

@zxdavb
Copy link
Owner

zxdavb commented Feb 8, 2021

Would you mind giving me a full sample scheme showing controller, dhw, ufh controller, trvs, zones etc

This is a cut and past job - will give the idea - note the mapping between UFH circuit and a evohome zone

Schema[01:145038 (evohome)] = {
    "controller": "01:145038",
    "system": {
        "heating_control": "13:237335",
        "orphans": [
            "04:225661",
            "04:225663"
        ]
    },
    "ufh_controllers": {
        "02:000921": {
            "ufh_circuits": {
                "00": {
                    "zone_idx": "03"
                },
                "01": {
                    "zone_idx": "06"
                },
                "02": {
                    "zone_idx": "04"
                }
            }
        },
    },
    "stored_hotwater": {
        "hotwater_sensor": "07:017494",
        "hotwater_valve": "13:109598",
        "heating_valve": null
    },
    "zones": {
        "00": {
            "heating_type": "zone_valve",
            "sensor": null,
            "devices": [
                "13:163420",
                "13:064044"
            ]
        },
        "01": {
            "heating_type": "underfloor_heating",
            "sensor": "34:190267",
            "devices": [
                "34:190267"
            ]
        },
...
        "0B": {
            "heating_type": "radiator_valve",
            "sensor": "22:098449",
            "devices": [
                "04:098449"
            ]
        }

@zxdavb
Copy link
Owner

zxdavb commented Feb 8, 2021

Also are you open to exposing some further properties/attributes

Everything you might need should be exposed, or example:

msg.verb, msg.src.id, msg.dst.id, msg.code, msg.payload = (
    "RQ", "01:123456", "13:234567", "3B00", {"...
)

Is there anything specific you think you might need, but you can't find?

@zxdavb
Copy link
Owner

zxdavb commented Feb 8, 2021

Invalid verb/code for 01:191706: RQ/0008

This is simply saying that an 01: device is not expected to send an RQ/0008 (which is true - they don't normally do this) - this suggests a corrupt pkt.

evohome_rf.exceptions.CorruptStateError: The system state is inconsistent: 13:102710 (BDR) has changed controller: 01:139901 to 01:191706 (try restarting the client library)

This confirms the diagnosis - the source address is corrupt (t was likely the HGI, which evohome_rf is getting to send RQ/0008 pkts).

This unfortunately common, and I have to work out a way of handling this. The controller doesn't have this issue, because it knows teh 'truth', whilst evohome_rf relies upon eavesdropping.

@zxdavb
Copy link
Owner

zxdavb commented Feb 8, 2021

I am not sure if you need to have evohome_rf maintain state? Possibly you can do the equivalent of this:

python client.py listen -rr /dev/ttyUSB0 ...

... and just consume (i.e. forward via MQTT) the messages?

Have you tried to do anything like:

python client.py monitor /dev/ttyUSB0 -o packet.log -x "RQ 01:123456 1F09 00"

...and I'm always keen on receiving packet logs to test against!

@smar000
Copy link
Author

smar000 commented Feb 9, 2021

Many thanks for the above - very helpful, and noted on the likely cause of the error message.

Have you tried to do anything like:

I haven't yet, but I should have the packet logs saved. Let's see how I get on, and if the problem persists, I will get back to you.

Everything you might need should be exposed

Thanks and yes, for the most part I found they are. One specific one that I didn't fine is the code_name in a Message. I had to lookup the name by importing CODE_NAMES from .message and checking against this.

Not a big deal TBH, but I thought it might be helpful to have this within the Message class itself.

Thanks again.

@zxdavb
Copy link
Owner

zxdavb commented Feb 9, 2021

You can use a packet log as a source, rather than a serial port - the will be useful fro test/dev:

cat packet.log | python client.py parse

I think code_name will be a useful addition!

Re: Opentherm - I am driven/limited by other projects, from which I am reluctant to deviate far... I see that JSON as distinct from my schema. E.g. https://github.com/mvn23/pyotgw

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

Re: Opentherm - I am driven/limited by other projects, from which I am reluctant to deviate far... I see that JSON as distinct from my schema. E.g. https://github.com/mvn23/pyotgw

Thanks and that is fine. After thinking about it, and seeing the different results under opentherm_msg, I think the way you have done it is logical. I have coded to take this into account.

With regards to sending commands, is there any callback that confirms the status of the sending, e.g. command sent/success/fail notifications? I see various packet messages showing retry/expired etc, but haven't (yet) looked to see how it all works in your code.

EDIT: Also is there any way to 'label' or tag devices? For example, if say there are multiple TRVs in a room, can we identify specific TRVs by name?

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

Another question - in your schema, what is the correct way to define underfloor_heating zone? In your example above, you gave:

 "01": {
            "heating_type": "underfloor_heating",
            "sensor": "34:190267",
            "devices": [
                "34:190267"
            ]
        },
  1. If I use the temperature sensor in the main controller itself for one zone, I assume we would put the controller ID for the sensor in that zone definition?

  2. If I have an HCC80R controlling UFH, how is this specified under devices for each individual zone, particularly given that tthe HCC80R has its own 'sub-zones' (as identified by ufh_idx in your message responses)? Would these sub-zones need to be specified? If so, where does this go?

Hope the above makes sense!

Thanks.

@zxdavb
Copy link
Owner

zxdavb commented Feb 10, 2021

OMG, the answer to this will be worth making into a wiki entry...

1st (useful to know): the output schema can be used as an input schema, so just use:

python client.py monitor /dev/ttyUSBx -x "RQ 01:xxxxxx 1F09 00"

and hit Ctrl-C after the last RP/01:xxxxxx/0418/xx - the schema should be there for you. Have you run the CLI yet - to do so is very useful, so get involved in that.

2nd: I have not had a UFH system to test against, so expect some issues. To answer this question:

If I have an HCC80R controlling UFH, how is this specified

Please start by providing me the schema, so we can discuss it without confusion.

3rd (the biggie): There are three means used by evohome_rf to construct the schema (in order of development):

  • from eavesdropping (by capturing packets between the controller and other devices)
  • from the input schema provided as a dict
  • from discovery (i.e. the controller is asked a series of RQ s, and the corresponding RP are used)

There are advantages to all 3 options - and each has issues, too. One is that when an received packet - usually an eavesdropped packet) does not match the schema: CorruptStateError (another reason why your input schema should be 'right').

The schema is important, it is used to construct some meta-data (e.g. zone heat demand from the child devices of that zone), and the objects (so you can, e.g. set the mode of a zones).

4th...

If I use the temperature sensor in the main controller itself for one zone

I just want to say, just want to avoid confusion: the schema should reflect reality: (evohome_rf doesn't change the behaviour of evohome), so 'making' the controller the sensor for a zone is only appropriate if it is the sensor for that zone.

There is no simple means to learn the sensor for a zone, when that sensor is the controller - I do have some code for this, but it is only 'nearly-deterministic' and is currently disabled. That is, the only 'hard/fast' way to know which zone the controller is a sensor for (if any) is via an input schema (which we hope is 'true' to reality for the above reasons).

@zxdavb
Copy link
Owner

zxdavb commented Feb 10, 2021

AttributeError: 'Serial' object has no attribute 'set_low_latency_mode'

What version of pyserial / pyserial-asyncio are you using?

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

Thanks for the above - very helpful. I did try the cli, but didn't realise that it built the full schema for me - definitely one for the wiki!

Schema output from discovery below:

Schema[01:139901 (evohome)] = {
    "controller": "01:139901",
    "system": {
        "heating_control": null,
        "orphans": []
    },
    "ufh_controllers": {
        "02:043392": {
            "ufh_circuits": {
                "00": {
                    "zone_idx": "07"
                },
                "01": {
                    "zone_idx": "0A"
                },
                "02": {
                    "zone_idx": "0B"
                }
            }
        }
    },
    "stored_hotwater": {
        "hotwater_sensor": "07:043555",
        "hotwater_valve": "13:133904",
        "heating_valve": "13:102710"
    },
    "zones": {
        "00": {
            "heating_type": "radiator_valve",
            "sensor": "34:015243",
            "devices": [
                "04:228044",
                "04:143928",
                "04:143926",
                "04:143922"
            ]
        },
        "01": {
            "heating_type": "radiator_valve",
            "sensor": "04:143924",
            "devices": [
                "04:227952"
            ]
        },
        "02": {
            "heating_type": "radiator_valve",
            "sensor": "04:228096",
            "devices": [
                "04:228042"
            ]
        },
        "03": {
            "heating_type": "radiator_valve",
            "sensor": "04:227954",
            "devices": [
                "04:227954"
            ]
        },
        "04": {
            "heating_type": "radiator_valve",
            "sensor": "04:000868",
            "devices": [
                "04:000868"
            ]
        },
        "05": {
            "heating_type": "radiator_valve",
            "sensor": "04:228048",
            "devices": [
                "04:228048"
            ]
        },
        "06": {
            "heating_type": "zone_valve",
            "sensor": "34:231315",
            "devices": [
                "13:230917"
            ]
        },
        "07": {
            "heating_type": "underfloor_heating",
            "sensor": null,
            "devices": []
        },
        "08": {
            "heating_type": "radiator_valve",
            "sensor": "04:001048",
            "devices": [
                "04:001048"
            ]
        },
        "09": {
            "heating_type": "radiator_valve",
            "sensor": null,
            "devices": [
                "04:228094"
            ]
        },
        "0A": {
            "heating_type": "underfloor_heating",
            "sensor": "34:112193",
            "devices": []
        },
        "0B": {
            "heating_type": "underfloor_heating",
            "sensor": "34:214769",
            "devices": []
        }
    }
}

I have an HCC80R installed, and so can give you any data that you may need from this.

Also in answer to your 4th point, yes, the scenario is "actual", in that my Dining Room zone (with ID "07" in the schema) is UFH, whose actuators are controlled by HCC80R, and whose temperature sensing is done by the main controller, which is situated in the dining room. I noticed that in the auto-discovered schema above, it is not showing any sensor for this zone "07"... I don't know if that is because it is confused with the controller being there, or may be I did not run the client long enough?

Given the importance of getting the schema correct, it might be helpful for end users if there was a pre-defined command for the cli that just went and got the schema and saved it to a file (or stdout) once the schema was complete. WDYT?

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

AttributeError: 'Serial' object has no attribute 'set_low_latency_mode'

What version of pyserial / pyserial-asyncio are you using?

pyserial is 3.4, and pyserial-asyncio is version 0.5

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

Just a follow on WRT the schema file. I added the above schema and tried to run the client with the schema as input. It is giving me the following error for the ufh_controller section:

client.py: Starting evohome_rf...
Traceback (most recent call last):
  File "client.py", line 370, in <module>
    cli()
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.7/dist-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.7/dist-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context().obj, *args, **kwargs)
  File "client.py", line 199, in monitor
    asyncio.run(main(lib_kwargs, command=MONITOR, **cli_kwargs))
  File "/usr/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/usr/lib/python3.7/asyncio/base_events.py", line 579, in run_until_complete
    return future.result()
  File "client.py", line 320, in main
    gwy = Gateway(lib_kwargs[CONFIG].pop(SERIAL_PORT, None), **lib_kwargs)
  File "/opt/scripts/evohome/evohome_rf/evohome_rf/__init__.py", line 103, in __init__
    self._schema, self.known_devices = load_schema(self, **kwargs)
  File "/opt/scripts/evohome/evohome_rf/evohome_rf/schema.py", line 216, in load_schema
    schema = SYSTEM_SCHEMA(kwargs.get("schema", {})) if kwargs.get("schema") else {}
  File "/usr/local/lib/python3.7/dist-packages/voluptuous/schema_builder.py", line 272, in __call__
    return self._compiled([], data)
  File "/usr/local/lib/python3.7/dist-packages/voluptuous/schema_builder.py", line 594, in validate_dict
    return base_validate(path, iteritems(data), out)
  File "/usr/local/lib/python3.7/dist-packages/voluptuous/schema_builder.py", line 432, in validate_mapping
    raise er.MultipleInvalid(errors)
voluptuous.error.MultipleInvalid: not a valid value for dictionary value @ data['ufh_controllers']

This is the ufh_controller section in the auto-discovered schema:

"ufh_controllers": {
        "02:043392": {
            "ufh_circuits": {
                "00": {
                    "zone_idx": "07"
                },
                "01": {
                    "zone_idx": "0A"
                },
                "02": {
                    "zone_idx": "0B"
                }
            }
        }
    },

Any suggestions as to what the issue may be?

Thanks.

@zxdavb
Copy link
Owner

zxdavb commented Feb 10, 2021

   "07": {
       "heating_type": "underfloor_heating",
       "sensor": null,
       "devices": []
   },

So simply change the above to have:

        "07": {
            "heating_type": "underfloor_heating",
            "sensor": "01:139901",
            "devices": []
        },

But! Don't be surprised it it doesn't work - I think I made some breaking changes - I will check it now.

Just checked: this seems to work OK (a minimal schema, the rest done by discovery):

    "schema": {
        "controller": "01:145038",
        "zones": {
            "00": {
                "sensor": "01:145038",
            }
        }
    }

Later, I'll test a full schema & let you know.

Of course, you can test it too.

@zxdavb
Copy link
Owner

zxdavb commented Feb 10, 2021

I noticed that in the auto-discovered schema above, it is not showing any sensor for this zone "07"... I don't know if that is because it is confused with the controller being there, or may be I did not run the client long enough?

As I said, above: there is no deterministic way to work out what zone the controller is a sensor for (if any) - you have to analyse the meta-data, and even then it's not fully guaranteed to be 'truth'.

The best scenario is that you have N+1 zones (presumably all with sensors), and only N sensors (other than the controller).

@zxdavb
Copy link
Owner

zxdavb commented Feb 10, 2021

Given the importance of getting the schema correct, it might be helpful for end users if there was a pre-defined command for the cli that just went and got the schema and saved it to a file (or stdout) once the schema was complete. WDYT?

Reasonable idea - will add it as a feature, but until then:

python client.py monitor /dev/ttyUSB0 > msg.out

TBH, you don't really need a schema, except for:

  • to ID your controller (vs that of your neighbours)
  • to ID the zone that the controller is a sensor for

Everything else can be discovered for evohome (that doesn't apply for other RAMSES-based systems, like Sundial, etc.).

In fact, it is probably better to keep it minimal, rather than risk it being wrong:

    "schema": {
        "controller": "01:123456",
        "zones": {
            "07": {
                "sensor": "01:123456",
            }
        }
    }

Note: evohome_rf will track all controllers (i.e. systems) it sees, but by default the first controller seen is the controller, (unless the allow/block lists come into play) - The schema allows you to ensure that this is your controller.

@zxdavb
Copy link
Owner

zxdavb commented Feb 10, 2021

I need a packet log from you

python client.py monitor /dev/ttyUSB0 -o packet.log

Just a follow on WRT the schema file. I added the above schema

Any suggestions as to what the issue may be?

Let me test it (as I said, I haven't done a lot of UFH testing).

Use this for now:

    "schema": {
        "controller": "01:139901",
        "zones": {
            "07": {
                "sensor": "01:139901"
            }
        }
    }

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

Later, I'll test a full schema & let you know.

Of course, you can test it too.

Thanks and yes, I will be testing too. Packet log with above schema attached:
packet.log

@smar000
Copy link
Author

smar000 commented Feb 10, 2021

Use this for now:

    "schema": {
        "controller": "01:139901",
        "zones": {
            "07": {
                "sensor": "01:139901"
            }
        }
    

Even starting with just the above, autodiscovery seems to overwrite the zone 7 sensor value to null.

@smar000
Copy link
Author

smar000 commented May 19, 2021

Is this expected behaviour?

No, if the callback expires, it should return False.

I haven't been seeing this. The callback doesn't seem to have been called on any of my expired commands that I could see.

Let me know what Command you're calling? & I'll take a look.

It isn't any specific Command, though more often with the more "complex" commands such as set_dhw_mode with an until. On occasion, some commands fail and have to be resent - I think it may be due to the distance between my arduino and the Controller (they are in different rooms). This doesn't always happen, but happens often enough that it would be helpful to know. In my earlier log paste, the expired command was actually a simple get_system_log_entry.

EDIT:
I have just now tried sending the command {"command": "get_system_log_entry", "kwargs" : {"log_idx": 0}} a few times. Almost all of them called the callback with the correct log entry. However, after repeated send attempts, one of them failed and showed the PktProtocolQos.send_data re-send/expired error, but as before did not call the callback (which is a simple print statement showing the callback response print(f"=================> rp_callback: {msg}") :

2021-05-20 06:45:13 |   | MQTT                  ->                       |  |                 | Received MQTT message: {"command": "get_system_log_entry", "kwargs" : {"log_idx": 0}}
PktProtocolQos.send_data(RQ|01:139901|0418|00): boff=2, want=RQ|01:139901|0418|00, tout=2021-05-20 06:45:13.905698: RE-SENT (1/3)
PktProtocolQos.send_data(RQ|01:139901|0418|00): boff=3, want=RQ|01:139901|0418|00, tout=2021-05-20 06:45:14.754533: RE-SENT (2/3)
2021-05-20 06:45:14 |000| HGI 18:134436         -> CTL Controller        |RQ| system_fault    | REQUEST: log_idx: 00
PktProtocolQos.send_data(RQ|01:139901|0418|00): boff=3, want=RQ|01:139901|0418|00, tout=2021-05-20 06:45:15.197516: RE-SENT (3/3)
2021-05-20 06:45:14 |000| HGI 18:134436         -> CTL Controller        |RQ| system_fault    | REQUEST: log_idx: 00
PktProtocolQos.send_data(RQ|01:139901|0418|00): boff=3, want=RP|01:139901|0418|00, tout=2021-05-20 06:45:15.226843: EXPIRED
2021-05-20 06:45:22 |053| OTB OpenTherm Bridge  ->                       | I| opentherm_sync  | ticker: 37662

Hope this helps.

On a separate note, do you know if it is possible to get an actuator to report its state (3EF0) by sending it an appropriate RQ command?

@zxdavb
Copy link
Owner

zxdavb commented May 20, 2021

OK, will have a look.

@smar000
Copy link
Author

smar000 commented May 20, 2021

A simple way to test is to send a command to a device that does not respond back, e.g, if I create a Command with the following arguments (code="3EF0", payload="00", dest_id="13:133904", verb="RQ"), this always resends/expires.

@zxdavb
Copy link
Owner

zxdavb commented May 20, 2021

OK, I found the bugs (x2) - it works now, but is not ideal - tests only when a packet arrives - if that packet is 15 seconds after the expiry, you have to wait 15 seconds. Below, the packet expired at 21:36:19.195, but the notice wasn't sent until immeditaely before the next packet at 21:36:42.583:

2021-05-20T21:36:15.727873  # evofw3 0.7.0
21:36:15.757 || HGI:006402 | NUL:------ |  I | puzzle_packet    | 01763... || {'ramses_rf': 'v0.9.7'}
21:36:18.766 || HGI:006402 | CTL:145039 | RQ | zone_name        | 0000     || {'zone_idx': '00'}
21:36:18.828 PktProtocolQos.send_data(RQ|01:145039|0004|00): boff=1, want=RQ|01:145039|0004|00, tout=2021-05-20 21:36:18.928404: RE-SENT (1/2)
21:36:18.850 || HGI:006402 | CTL:145039 | RQ | zone_name        | 0000     || {'zone_idx': '00'}
21:36:18.961 PktProtocolQos.send_data(RQ|01:145039|0004|00): boff=2, want=RQ|01:145039|0004|00, tout=2021-05-20 21:36:19.161328: RE-SENT (2/2)
21:36:18.982 || HGI:006402 | CTL:145039 | RQ | zone_name        | 0000     || {'zone_idx': '00'}
21:36:19.195 PktProtocolQos.send_data(RQ|01:145039|0004|00): boff=2, want=RP|01:145039|0004|00, tout=2021-05-20 21:36:19.183045: EXPIRED
21:36:42.583 MsgTransport._pkt_receiver(RP|01:145039|0004|00): Expired callback
Callback has expired
21:36:42.583 || STA:205645 |            |  I | temperature      | 00077D   || {'temperature': 19.17}

I will fix it presently.

@zxdavb
Copy link
Owner

zxdavb commented May 20, 2021

Try 0.9.7 - only the above issue remains.

@smar000
Copy link
Author

smar000 commented May 20, 2021

Try 0.9.7 - only the above issue remains.

Working! Thanks again for the quick turn-around.

@zxdavb
Copy link
Owner

zxdavb commented May 21, 2021

I have pushed an update to master that will make callbacks for failed Command much faster. Something like:

2021-05-20T21:36:15.727873  # evofw3 0.7.0
21:36:15.757 || HGI:006402 | NUL:------ |  I | puzzle_packet    | 01763... || {'ramses_rf': 'v0.9.7'}
21:36:18.766 || HGI:006402 | CTL:145039 | RQ | zone_name        | 0000     || {'zone_idx': '00'}
21:36:18.828 PktProtocolQos.send_data(RQ|01:145039|0004|00): boff=1, want=RQ|01:145039|0004|00, tout=2021-05-20 21:36:18.928404: RE-SENT (1/2)
21:36:18.850 || HGI:006402 | CTL:145039 | RQ | zone_name        | 0000     || {'zone_idx': '00'}
21:36:18.961 PktProtocolQos.send_data(RQ|01:145039|0004|00): boff=2, want=RQ|01:145039|0004|00, tout=2021-05-20 21:36:19.161328: RE-SENT (2/2)
21:36:18.982 || HGI:006402 | CTL:145039 | RQ | zone_name        | 0000     || {'zone_idx': '00'}
21:36:19.195 PktProtocolQos.send_data(RQ|01:145039|0004|00): boff=2, want=RP|01:145039|0004|00, tout=2021-05-20 21:36:19.183045: EXPIRED
21:36:19.195 PktProtocolQos.send_data(RQ|01:145039|0004|00): Expired callback
Callback has expired
21:36:42.583 || STA:205645 |            |  I | temperature      | 00077D   || {'temperature': 19.17}

@smar000
Copy link
Author

smar000 commented May 22, 2021

Hi again @zxdavb

One thing to clarify - is it still the recommendation that the schema should only contain the controller ID and allow_list, and leave everything else for auto-discovery?

@zxdavb
Copy link
Owner

zxdavb commented May 22, 2021

Yes, Controller ID only, and an allow_list - there are a few exceptions:

  • OTB can't be discovered, only specified in schema or eavesdropped
  • some TRVs, 00:, can't be discovered (so, same as above)

Eavesdropping is not generally recommended, but is needed to confirm a schema.

@smar000
Copy link
Author

smar000 commented May 23, 2021

Yes, Controller ID only, and an allow_list - there are a few exceptions:

  • OTB can't be discovered, only specified in schema or eavesdropped
  • some TRVs, 00:, can't be discovered (so, same as above)

Eavesdropping is not generally recommended, but is needed to confirm a schema.

Ok thanks and noted.

@zxdavb
Copy link
Owner

zxdavb commented May 24, 2021

Yes - may need allow_list off & eavesdropping on to get full schema - then make a schema, use an allow_list & turn off eavesdropping for ongoing, until any changes are made at some future time.

TBH, you can manage without a full schema in most circumstances, but an allow_list is virtually required.

I use the allow list in ramses_rf, but also in evohome_cc too - that it, you may want to consider using it in evohome_listener.

@smar000
Copy link
Author

smar000 commented May 24, 2021

Thanks and that makes sense. I will look to do it the same way.

TBH, you can manage without a full schema in most circumstances, but an allow_list is virtually required.

For my own understanding, the allow_list is required to prevent messages appearing from (a) neighbouring evohome setups and (b) messages getting garbled in reception thus appearing to be from incorrect device ids?

@zxdavb
Copy link
Owner

zxdavb commented May 24, 2021

Yes (to both).

The big issue is that valid packets can - as you say - have corrupted (but still valid) devices addresses.

There are a lot of controls in place for this, but ultimately an allow list is required.

@smar000
Copy link
Author

smar000 commented May 24, 2021

Understood! Thanks.

@smar000
Copy link
Author

smar000 commented May 26, 2021

@zxdavb Looking at client.py, is it sufficient to pass the --discover argument in order to enable eavesdropping for the full schema?

Also when should we use the SCAN_DISC, SCAN_FULL, SCAN_HARD and SCAN_XXXX?

Thanks.

@zxdavb
Copy link
Owner

zxdavb commented May 26, 2021

(not sure if I understand your question 100%, so here goes)

Also when should we use the SCAN_DISC, SCAN_FULL, SCAN_HARD and SCAN_XXXX?

These are used for probing new device types or for development - not for production - discovery is a separate thing, and is built into the library.

The client.py tool's main reason for being is to: a) perform ad-hoc stuff / learn about the protocol (e.g. SCAN_FULL), or to demonstrate how the library can be used (e.g. evohome-listener, or evohome_cc). I see you've pulled some ideas from client.py (which the idea), but you may also want to look at evohome_cc\__init__.py (although it is a little more obtuse).

All the CLI switches are a 'cheat' - all that matters is passing the config/schema/device_list kwargs to the library - you can get those parameters from any source you like - from CLI/yaml/json/etc. - or a config file in your case.

Generally, eavesdropping is to be avoided, especially without an allow_list (although it may be the only way to get your allow_list, but don't eavesdrop after that.)

I use voluptuous roughly the same way you use configparser, and so see my schema.py:

CONFIG_SCHEMA = vol.Schema(
    {
        vol.Optional(DISABLE_DISCOVERY, default=False): bool,
        vol.Optional(DISABLE_SENDING, default=False): bool,
        vol.Optional(ENABLE_EAVESDROP, default=False): bool,
        vol.Optional(REDUCE_PROCESSING, default=0): vol.All(
            int, vol.Range(min=0, max=DONT_CREATE_MESSAGES)
        ),
        vol.Optional(MAX_ZONES, default=DEFAULT_MAX_ZONES): vol.All(
            int, vol.Range(min=1, max=16)
        ),
        vol.Optional(USE_SCHEMA, default=True): vol.Any(None, bool),
        vol.Optional(ENFORCE_ALLOWLIST, default=None): vol.Any(None, bool),
        vol.Optional(ENFORCE_BLOCKLIST, default=None): vol.Any(None, bool),
        vol.Optional(USE_NAMES, default=None): vol.Any(None, bool),
        vol.Optional(EVOFW_FLAG, default=None): vol.Any(None, str),
    },
    extra=vol.ALLOW_EXTRA,  # TODO: remove for production
)

There is one thing that ramses_rf needs to do, and does not yet do well - that is periodically re-discover...

This would be most useful or OTBs (R8810As) - I will ad this soon enough.

HTH, ask again if not.

@smar000
Copy link
Author

smar000 commented May 26, 2021

Great, that clears up quite a bit. Will also have a look at your evohome_cc_init.py_.

Thanks again.

@smar000
Copy link
Author

smar000 commented May 28, 2021

Hi David

I noticed that zone names are available in .evo.params but not in the schema's zone collection. Should /can these param zone names also be loaded into the framework in some way at start up, to save having to wait for the system to populate itself from eavesdropping (which can take some time)?

@zxdavb
Copy link
Owner

zxdavb commented May 28, 2021

I am happy with the split between schema (effectively static, unless bindings are destroyed/created), params (configurable), and status (variable, but read-only) - the only possible

I am happy to push in (impose) a (hand-crafted) schema, only because it is so static - doing so with params would be problematic, and inappropriate with status...

... however, the latter two can be managed with _get_state(), _clear_state() and _set_state() - have a look at those - although they are currently private methods, they will eventually become canon.

There are two bugs presently affecting _set_state():

  • some packets are being expired too early
  • packets are not being refreshed when they do expire

HTH

@zxdavb
Copy link
Owner

zxdavb commented May 28, 2021

BTW, you make a good point - eavesdropping of schema if problematic & disabled by default, but eavesdropping of params/state is normal practice.

@smar000
Copy link
Author

smar000 commented May 29, 2021

however, the latter two can be managed with _get_state(), _clear_state() and _set_state() - have a look at those - although they are currently private methods, they will eventually become canon.

Thanks, will check these out. Noted on the bugs.

@smar000
Copy link
Author

smar000 commented May 29, 2021

but eavesdropping of params/state is normal practice.

Ok!

@smar000
Copy link
Author

smar000 commented Jun 1, 2021

Hi David

Thanks for all your help (and patience!) throughout the migration. I am almost there I think. One final (hopefully!) question for now WRT the allow_list. Is this supposed to only permit exactly those devices listed or will it automatically allow any "18:" HGI device types through?

I ask as I have a number of spurious messages purporting to be from HGI devices (e.g. I am seeing messages from the following HGI IDs now as I type: 18:000030, 18:000070, 18:007030, 18:056026, 18:136712; these are all in addition to my actual ID of 18:000730).

My allow_list (as shown at initialisation) appears to be correctly configured and active and ENFORCE_ALLOWLIST also set to true:

Using an allow_list: ['01:139901', '02:043392', '04:000868', '04:001048', '04:143922', '04:143924', '04:143926', '04:143928', '04:227936', '04:227952', '04:227954', '04:228032', '04:228042', '04:228044', '04:228048', '04:228094', '04:228096', '04:228104', '07:033469', '07:043555', '10:142568', '13:230917', '13:102710', '13:133904', '13:127198', '34:015243', '34:112193', '34:214769', '34:231315', '18:000730']

Anyway, thanks again!

@smar000
Copy link
Author

smar000 commented Jun 1, 2021

Sorry, yet another query/clarification. I noticed that zone/dhw mode "until" datetimes are formatted (in the helper.) without the T separating date and time.

Should the T be included as part of the isoformat (e.g. https://en.wikipedia.org/wiki/ISO_8601)?

@smar000
Copy link
Author

smar000 commented Jun 2, 2021

Hi again @zxdavb

One further query - I noticed that you have the default HGI device address in the consts.py file as 18:000730. However, the address of my nanoCUL appears to be 18:136712. I don't see this address in the eavesdropped schema. Is there any way to identify the nanoCUL by the framework?

e.g. some messages from my log file:

2021-06-02 07:19:17,441 [299] || HGI:136712         | OpenTherm Bridge   | RQ | opentherm_msg    | 00000... || {'msg_id': '0x03', 'msg_name': 'SlaveConfigFlags', 'msg_type': 'Read-Data', 'description': 'Slave configuration'}
2021-06-02 07:19:17,760 [299] || HGI:136712         | Controller         | RQ | zone_devices     | 0208     || {'zone_idx': '02', 'device_class': 'rad_actuators'}
2021-06-02 07:19:17,776 [299] || Controller         | HGI:136712         | RP | zone_devices     | 02080... || {'zone_idx': '02', 'device_class': 'rad_actuators', 'devices': ['04:228042']}
2021-06-02 07:19:17,856 [299] || HGI:136712         | DHW Relay          | RQ | tpi_params       | 00       || {}
2021-06-02 07:19:17,873 [299] || DHW Relay          | HGI:136712         | RP | tpi_params       | 00300... || {'cycle_rate': 12, 'min_on_time': 1.0, 'min_off_time': 0.0, '_unknown_0': '00', 'proportional_band_width': None, '_unknown_1': '01'}

@zxdavb
Copy link
Owner

zxdavb commented Jun 3, 2021

Currently. 18: devices are never filtered - this causes issues, and needs to change.

Because you don't know the HGI80s address before you send a packet, you use 18:000730, and it 'translates' it to it's actual device address, 18:136712 in your case.

Is there any way to identify the nanoCUL by the framework?

This is planned.

Sorry, yet another query/clarification. I noticed that zone/dhw mode "until" datetimes are formatted (in the helper.) without the T separating date and time.

I am willing to take a view on this (the issue is whether it will be a breaking change), but is is kind of 'optional', see: https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat

@smar000
Copy link
Author

smar000 commented Jun 3, 2021

Currently. 18: devices are never filtered - this causes issues, and needs to change.

Because you don't know the HGI80s address before you send a packet, you use 18:000730, and it 'translates' it to it's actual device address, 18:136712 in your case.

Understood!

I am willing to take a view on this (the issue is whether it will be a breaking change), but is is kind of 'optional'

Ah ok, I wasn't aware that it was optional. It's not that much of a problem, but openHAB's mqtt binding requires the "T" out of the box. Without it, additional formatting steps need to be carried out which has to be repeated for each datetime item etc. For now, I've added a small conversion before publishing to the mqtt broker. Maybe make it optional via config If you do re-consider it in the future?

@zxdavb
Copy link
Owner

zxdavb commented Jun 3, 2021

Maybe make it optional via config If you do re-consider it in the future?

Making it a config option is not useful - I'll change the format if I can.

@smar000
Copy link
Author

smar000 commented Jun 4, 2021

Making it a config option is not useful - I'll change the format if I can.

Thanks. As I said, not a significant issue from my side.

Anyway, my migration to your framework seems to be complete for now, and so I will close this thread. Sincere thanks once again for all your support throughout.

@smar000 smar000 closed this as completed Jun 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants