Skip to content

WIP Create superclass for insteon thermostats #105

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

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

wz2b
Copy link
Contributor

@wz2b wz2b commented Sep 8, 2018

  • Create a subclass for insteon thermostats and split 2441TH and 2441V devices. Currently these are identical but they won't be for long, as I don't want to introduce "universal" additions that I can't test against a device I don't have
  • In the IPDB, removed the "product code" matching requirement from ipdb for the 2441V adapter. The documentation says 0x0000001F is correct but that's not what the device actually sends.
  • In climate control device model, fixed references to self._update_method that should have been self._updatemethod. Long term this should be either added to the constructor or made into a proper property, but doing that means touching a lot of device/state code that I can't currently test

wz2b added 13 commits September 6, 2018 22:19
…low creating an interface then manually triggering an aldb refresh later
…ances, one for the 2441TH (which already existed) and one for the 2441V (currently adding).
…ances, one for the 2441TH (which already existed) and one for the 2441V (currently adding).
of the Insteon thermostat.  Currently they are identical, but they
won't be shortly.

Fixed an apparent bug in states/thermostat where it referred to _update_method
when the instance member of state is actually named _updatemethod.  Note that
this really should either be included in the constructor (best choice) or
added as a proper property.  If the convention is that things starting with _
are private to the class, then external classes shouldn't be reading or
writing them.
…houdln't have been there - that was intended for the backlight configuration command that isn't debugged yet
… to spacing and line length more to satisfy pylint, and not make last-second changes just prior to commit without running tox first.
Copy link
Collaborator

@teharris1 teharris1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indicates there is no difference between the 2441th and the 2441v. Is that correct?

class ClimateControl_2441th(ClimateControl_Base):
"""TH2441TH thermostat model."""

def __init__(self, plm, address, cat, subcat, product_key=None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need init if all it does is call super().init

class ClimateControl_2441v(ClimateControl_Base):
"""TH2441V thermostat adapter model."""

def __init__(self, plm, address, cat, subcat, product_key=None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need init if all it does is call super.init()

@wz2b
Copy link
Contributor Author

wz2b commented Sep 9, 2018 via email

@wz2b
Copy link
Contributor Author

wz2b commented Sep 9, 2018 via email

@wz2b
Copy link
Contributor Author

wz2b commented Sep 9, 2018

The states themselves (in states/thermostat.py) have the message parsing in them, which I think is a problem because the extended status message that comes back from a 2441TH is different than a 2441V.

Even for the TH, I'm having problems really lining up what I see in the code vs. the developer's guide.

        self._message_callbacks.add(temp_msg, self._temp_received)
        ext_status_recd = ExtendedReceive.template(
            commandtuple=COMMAND_EXTENDED_GET_SET_0X2E_0X00,
            cmd2=0x02,
            userdata=Userdata.template({"d1": 0x01}))
        self._message_callbacks.add(ext_status_recd,
                                    self._ext_status_received)

On a ZTH, when you get back a cmd1=0x2E, cmd2=0x02, Data1=0x01 you get back this:

  • Data1 = 1
  • Data2 = status flag
  • Data3 = hour
  • Data4 = minute
  • Data5 = second
  • Data6 = SysMode
  • Data13 = CRC High
  • Data14 = CRC Low

The actual extended status message I think we want is
cmd1=0x2e, cmd2=0x00, d2=0x01
which returns

  • Data2: 1 return of data
  • Data3: LocalTempHighByte
  • Data4: LocalTempLowByte
  • Data5:Humidity
  • Data6: TempOffset
  • Data7: HumiOffset
  • Data8 : System Mode
  • Data9 Fan Mode
  • Data10: Backlight Seconds
  • Data11: Hysteresis minutes to turn on AC
  • Data12: Energy set back point
  • Data 13: ACKByte
  • Data 14: FWRev

Note again that's still different than what a 2441V returns.

I can go ahead and try to fix all of this, but I think the thing that has to happen is that the specific message parsing has to be removed from the states. Either that or the states need to call back into the device-specific model (e.g. ClimateControl_2441TH) and ask how to parse the messages.

I can't really proceed without some guidance.

2441THdev-062012-en.pdf

@teharris1 teharris1 changed the title Create superclass for insteon thermostats WIP Create superclass for insteon thermostats Sep 9, 2018
@wz2b
Copy link
Contributor Author

wz2b commented Sep 11, 2018

After working through several scenarios I decided the best way to do this is to take all the message-parsing out of the states and let them live in the main module. This way you can have the same set of "states" shared among different types of thermostats and adapters.

To accomplish this, the states - for example Temperature - will have a set() method that takes a temperature value, in degrees. Then it's up to the individual Climate_XXX module to figure out how to receive the message (for example extended status message), parse it according to the device Developer Notes manual for that specific adapter, and call the more generic update_value method for that state. The ACK handlers will do the same, meaning the semantics of "Launch a set() request but don't update my state until I receive confirmation the device got the message" still holds.

Feedback on this is entirely welcome. If anyone has a 2441TH who is willing to help me test that I don't break that please advise. I did find one on ebay and made a low bid, just to have it for development.

@wz2b
Copy link
Contributor Author

wz2b commented Sep 11, 2018

Why does State need group, linkdata1, linkdata2, linkdata3, is_controller, and is_responder? Why are these part of "State" ? Is there a good reason? The accessors don't seem to be called anywhere. I don't think they belong there. What am I missing?

@teharris1
Copy link
Collaborator

teharris1 commented Sep 11, 2018 via email

@teharris1
Copy link
Collaborator

The reason the message handling is inside State is because this makes it clear what messages effect what state of the device. Multiple states can listen/subscribe to a single message. The alternative is one method in the device needs to hand changes to each state that is effected or the state needs to listent/subscribe to a change made by the device. The problem with the device handing changes to the state is that the state is no longer controlling itself. This breaks encapsulation. Notice that today all state changes are managed by the state itself either through contained methods (i.e on(), off(), set_level(), etc.) or via a message received. I had really considered having the device be the master of all message intake but decided on the current method as a way to encapsulate the effect any message has on a single state. This also makes states more reusable across devices and as base states for more complex states.

@wz2b
Copy link
Contributor Author

wz2b commented Sep 11, 2018

For any device that has multiple states/groups, the _states list will be used to create the default links needed between the device and the modem (i.e. PLM or Hub).

I feel like this breaks The Single Responsibility Principle of S.O.L.I.D. The fact that needed to be explained kind of shows that - when I saw it, I thought "Why is this here?". If you haven't had time to finish, might you reconsider this? At least use the wiki to explain the OOP responsibilities of a State vs. a Device, then take a step back and see where you're going still makes sense.

I had really considered having the device be the master of all message intake but decided on the current method as a way to encapsulate the effect any message has on a single state.

I see. In terms of single responsibility, I think Temperature and Humidity are their own things: a subclass of State:

An object is an entity that has state, behavior, and identity. The structure and behavior of similar objects are defined in their common class. The terms instance and object are interchangeable. Booch

and being common I'd like to reuse it between devices. The problem is with these thermostat status messages is that they conflict between the TH, ZTH, and V.

The reason the message handling is inside State is because this makes it clear what messages effect what state of the device.

True, but look at it the other way: you spread the "protocol" out across multiple files. In the case of the status message, you actually spread the handling for that message across multiple classes. I found it more confusing because I think they are really two different concepts:

  • An observable object that represents the current temperature in the room
  • An object that handles an incoming 0x6E message, or one byte of an 0x2E message

I'm not saying that the state objects necessarily have to be dumb osbservable beans (though I don't see the problem with that) but I feel like these are different responsibilities.

For any device that has multiple states/groups, the _states list will be used to create the default links needed between the device and the modem

I have until now been managing links externally. I wouldn't mind seeing things combined in some way but you haven't convinced me that it belongs in State. You're giving State yet a third job to do. I am separately thinking about what happens when you persist the state vs. persist the links. And I think the links are a facet of the protocol, not of "Humidity - an object that represents the humidity in a particular room."

Is this good discussion? I'm operating along the spirit of you having said:

I would love to see how someone else can work with my code and create one of the more complex devices.

which means feedback and discussion. Hope that's what you intended.

@teharris1
Copy link
Collaborator

Yes, this is exactly what I was hoping for. The only thing is perhaps we can take it out of this PR and make it an issue or part of a discussion thread somewhere, given that we are getting more into discussions that should stay persistent with the project. Let me think about where that should go.

First let me admit that the default link data sitting in the State class was not meant to hit the master branch. It got there by accident because I figured out while codeing the thermostat that default links with the termostat are critical to the overall success of that device. So that code was just playing around. In then decided to take a step back and fix a bunch of persistent lint issue and accidently used the branch that I had been playing with default links. (Hense the value of code reviews to find silly mistakes like that.)

So you make a lot of really good points and I see what you mean regarding State being too extensive in it's purpose. I am not convinced however, that it is best to bring it up to the Device level. I am not sure how that actually solves your concern but only moves the concern up to the device level. What then is the purpose of the Device object?

I have looked at a lot of Insteon implementations and the one that I modeled this most closely to is openHAB. Most other implemetations put everthing on the device. This can be very clean if you just want to turn a light on or off. But when you look at the more complex devices it becomes a lot more difficult to have a consistant implementation and the Device class winds up being just a collection of methods. In the openHAB model, a device is a collection of features. A feature a message handler and command generator. So in many respects the State class is similar to the openHAB feature. I do agree with you that I probably put too much on the State class at this time. But I also think I have put too much on the Device class too.

At the end of the day, I am not emotionally tied to any of the architecture of this module. I have known for a while that I need to rearchitect this to make it easier for future developers but have not sat down to actually do it. And give that it is now supporting Hubs, it is no longer just a PLM module. I have recently reserved the pyinsteon name in GitHub and am looking to post a dummy module to pypi to reserve the name there. That will become the 1.0.0 version of this module.

There are only a few things I feel strongly about with the design of this module:

  1. The module is build for consistent and reliable handling of Insteon devices. I am choosing reliability over speed. This is a really important point. There are a lot of things that could be done to make this library faster and the advantage of that would be to allow this module to handle your Christmas light display. With the current design, that would not be possible. It is too slow. But it is consistent and will run your house 24x7x365 without issue. Perhaps in the future speed can be a programmer setting but for now consistency and reliability is the goal.
  2. Message intake has to be truly asyncronous. Most, if not all, implementations I have looked at assume a send/respond pattern that is not valid. (i.e. I send an 'on' message, get a message ACK so it must be good. Or I send an 'on' message, get a message ACK and the next message must be a device ACK.). At the end of the day, the underlying protocol is truly asyncronous (albeit in a serial model), and messages can appear without warning when the device is itneracted with by the human.
  3. Devices must be discoverable and not require a config file to define them. While I gave in to this with battery opperated devices, that was just due to assuming that the user was coming from another piece of software and the link was set up already. If you do link management correctly the device is always known. Additionally, even battery operated devices can be discovered if you know how to talk to them (i.e wait for them to talk first, then ask the question.

A fourth goal is asperational and I could be talked out of ut but:
4) Devices should be implemented in a consistent pattern regardless of the device complexity. This will allow programmers (who are the real users of this module) to discover the device's capabilities rather than have to know them to implement them. This one is more aspirational right now but hoping to get there.

@wz2b
Copy link
Contributor Author

wz2b commented Sep 11, 2018 via email

@teharris1
Copy link
Collaborator

teharris1 commented Sep 11, 2018 via email

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

Successfully merging this pull request may close these issues.

2 participants