Skip to content

Style change follow up#13707

Merged
kellerza merged 12 commits intohome-assistant:devfrom
cdce8p:homekit-static
Apr 11, 2018
Merged

Style change follow up#13707
kellerza merged 12 commits intohome-assistant:devfrom
cdce8p:homekit-static

Conversation

@cdce8p
Copy link
Copy Markdown
Member

@cdce8p cdce8p commented Apr 5, 2018

Follow up to: #13654

Description:

During the last style change @kellerza suggested to rework what is passed into the HomeAccessory constructor. I think to following changes might make sense:

  • hass is now a class variable of HomeAccessory and HomeBridge and set during the HomeKit setup. That way it doesn't need to be passed to each accessory individually.
  • For the HomeAccessory I added the method update_state_callback which checks that the new state isn't None (previously done in each type) and calls the update_state method.

Checklist:

  • The code change is tested and works locally.
  • Local tests pass with tox. Your PR cannot be merged unless tests pass
  • Tests have been added to verify that the new code works.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should not do this.

All of Home Assistant is coded in a way that binds all gobal state to the hass object. Class variables go against this. Pass it into the constructor or set it as an instance variable. Don't set it as a class variable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This signature looks much better, if you add hass here, it should adress @balloob's concerns as well.

Also means no change required in __init__.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 This is a good idea and seem to simplify things as well

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also leave hass in here, and try to keep same signature as __init__ in HomeAccessory (at the moment you swap name & entity_id).

Maybe keep name first, since Accessory's signature is (name, **kwargs)

*same for other HomeAccesories

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you call category with a keyword it also protects against future changes

super().__init__(name, entity_id, category=CATEGORY_*, **kwargs)

*same for all HomeAccessories

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once all Accesory signature are the same you can simplify this:

def get_accessory(hass, state, aid, config):
    """Take state and return an accessory object if supported."""
    if not aid:
        _LOGGER.warning('The entitiy "%s" is not supported, since it '
                        'generates an invalid aid, please change it.',
                        state.entity_id)
        return None

    a_type = None
    a_kwargs = {'aid'} = aid

    if state.domain == 'sensor':
        unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
        if unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
            a_type = 'TemperatureSensor'
        elif unit == '%':
            a_type = 'HumiditySensor'

    elif state.domain == 'cover':
        # Only add covers that support set_cover_position
        features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
        if features & SUPPORT_SET_POSITION:
            a_type = 'WindowCovering'

    elif state.domain == 'alarm_control_panel':
        a_type = 'SecuritySystem'
        a_kwargs['alarm_code'] = config.get(ATTR_CODE)

    elif state.domain == 'climate':
        features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
        support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
            SUPPORT_TARGET_TEMPERATURE_HIGH
        # Check if climate device supports auto mode
        a_kwargs['support_auto'] = bool(features & support_temp_range)
        a_type = 'Thermostat'

    elif state.domain == 'light':
        a_type = 'Light'

    elif state.domain in ('switch', 'remote', 'input_boolean', 'script'):
        a_type = 'Switch'
    
    if a_type is None:
        return None

    _LOGGER.debug('Add "%s" as "%s"', state.entity_id, state.name)
    return TYPES[a_type](hass, state.name, state.entity_id, **a_kwargs)

@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 8, 2018

Thanks for the feedback. I wasn't home this weekend, so I haven't had the chance to do the improvements yet. Hopefully during the next few days, although I have already done a rebase.

cdce8p added 3 commits April 10, 2018 00:06
* Set argument order for HomeAccessory and HomeBridge
* Added update_state_callback
@cdce8p cdce8p changed the title Style change follow up WIP: Style change follow up Apr 9, 2018
@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 9, 2018

I have update the commits to reverse the change to make hass a static var and incorporated the over feedback. Especially the change of get_accessory as suggested by @kellerza.

'generates an invalid aid, please change it.',
state.entity_id)
return None
return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although they are the same, I would suggest we keep return None where we care / intend to use the return of a function

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have again added it to get_accessory and generate_aid. In cases where it is an abort condition (like HomeKit.start(), HomeKit.stop(), HomeAccessory.update_state_callback()) I would prefer to stay with just return.

@cdce8p cdce8p changed the title WIP: Style change follow up Style change follow up Apr 10, 2018
self.hass = hass
self.entity_id = entity_id
super().__init__(hass, name, entity_id,
category=CATEGORY_WINDOW_COVERING, **kwargs)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Personally I would really like to move these calls to the HomeAccessory class somehow. Any ideas who to improve that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

On solution could be to use a decorator function for for the init method which calls super().__init__(). That would look like this:

def super_init(category):
    def category_wrapper(func_init):
        def wrapper(self, hass, name, entity_id, **kwargs):
            super(self.__class__, self).__init__(
                hass, name, entity_id, category=CATEGORY_LIGHT, **kwargs)
            func_init(self)
        return wrapper
    return category_wrapper


@TYPES.register('Light')
class Light(HomeAccessory):

    @super_init(CATEGORY_LIGHT)
    def __init__(self):

Should we do that or stay at the current:

    def __init__(self, hass, name, entity_id, **kwargs):
        super().__init__(hass, name, entity_id, category=CATEGORY_LIGHT, **kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't like the decorator and not sure how that will interfere with __init__

You could do this (note: category= at the end so they look the same at first glance)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs, category=CATEGORY_LIGHT)

but being explicit might give you some code editor hints, so I like this version more:

    def __init__(self, hass, name, entity_id, **kwargs):
        super().__init__(hass, name, entity_id, **kwargs,
                         category=CATEGORY_LIGHT)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The idea behind the decorator was to limit code redundancy between different types. You're right the it might be more difficult for editors to read, however I don't think that is that big of a deal since those attributes are already assigned in accessories.py/HomeAccessory and most inexperience developers will only copy existing types without fully understanding them.

Regarding the interference with __init__, it should change anything. The call order is as follows:

  1. HA start
  2. Module is loaded and __init__ call is replaced by decorater
  3. Call to light.__init__
  4. super().__init__()
  5. __init__()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you dont like the super rather add a bootstrap/postinit method in the child classes that you call from the parent’s init. Type could be a class var on the child

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In HomeAccessory you then have:

    def __init__(self, hass, name, entity_id, **kwargs):
        """Initialize a Accessory object."""
        super().__init__(name, **kwargs)
        set_accessory_info(self, name, model=entity_id)
        self.entity_id = entity_id
        self.hass = hass
        cat = self.init_category()
        self.category = getattr(Category, cat, Category.OTHER)

with only def init_category(self) and no more __init__ in the rest, init_category then returns the category.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Extending on your idea what do you think about the following:

  • Making the category a static var of the type
  • Changing the __init__() of the types to something like init_setup() which is called from HomeAccessory.__init__()

Copy link
Copy Markdown
Member Author

@cdce8p cdce8p Apr 10, 2018

Choose a reason for hiding this comment

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

init_setup() might not be the best name. If you have something else in mind, I'm happy to change it.
The only caveat with this solution is, that I had to add # pylint: disable=attribute-defined-outside-init to every type file.

@cdce8p cdce8p changed the title Style change follow up WIP: Style change follow up Apr 10, 2018
@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 10, 2018

@kellerza What do you think about a helper function to create characteristics? I thought about something like this (example from type_lights, char_on, code):

# Class HomeAccessory

    def setup_char(self, service, char_name, char_var_name,
                   value=None, callback=None):
        char = self.__dict__[char_var_name] = \
            service.get_characteristic(char_name)
        if value:
            char.value = value
        if callback:
            char.setter_callback = callback
# Type lights (init / init_setup)

        self.setup_char(serv_light, CHAR_ON, 'char_on',
                        value=self._state, callback=self.set_state)

Copy link
Copy Markdown
Member

@kellerza kellerza left a comment

Choose a reason for hiding this comment

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

From the code trying to remove super().__init__(x) is forced.

Rather just stick to

    super().__init__(*args)

From you changes it does not seem like **kwargs is required any longer, only aid

a_type = 'Switch'

if a_type is None:
else:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change will cause issues....
state.domain == 'sensor' and unit is not %, or temp and a_type is undefined

Please revert to the original if a_type is None:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right. Didn't thought about that.

return None

a_type = None
a_kwargs = {'aid': aid}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Keep a_type = None and config = {}

return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
a_type = 'Switch'

else:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if a_type is None:

state.entity_id, 'HumiditySensor')
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
aid=aid)
if state.domain == 'alarm_control_panel':
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Insert a_type = None

@@ -1,4 +1,5 @@
"""Class to hold all cover accessories."""
# pylint: disable=attribute-defined-outside-init
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be move to def init_setup

@@ -1,4 +1,5 @@
"""Class to hold all alarm control panel accessories."""
# pylint: disable=attribute-defined-outside-init
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be move to def init_setup

@@ -1,4 +1,5 @@
"""Class to hold all sensor accessories."""
# pylint: disable=attribute-defined-outside-init
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be move to def init_setup

@@ -1,4 +1,5 @@
"""Class to hold all switch accessories."""
# pylint: disable=attribute-defined-outside-init
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be move to def init_setup

@@ -1,13 +1,16 @@
"""Class to hold all thermostat accessories."""
# pylint: disable=attribute-defined-outside-init
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be move to def init_setup


def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
def __init__(self, hass, name, entity_id, config, **kwargs):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With config, can you not change **kwargs into aid?

@kellerza
Copy link
Copy Markdown
Member

Regarding setup_char, can't you bake it into add_preload_service? But once again, might be adding too many helpers 😕 On how many instance can you apply this?

@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 10, 2018

setup_char would be used for every char added in __init__. E.g. for lights: max 5 times. The idea behind all this is to reduce the type classes to a minimum to make adding new ones and keeping HAP-python up-to-date easier.

Update: In theory a combination with add_preload_service might work. However the method call would probably be enormous.


Regarding pylint: disable: It really doens't matter where it's defined. Unless it's tied to a statement, the scope is from the pylint till the end of the file (at least as far as I know). I personally would still favor the decorator solution. Especially to keep things short in the types files. They shouldn't worry about name, entity_id and hass. An additional argument would be that it's way to easy to swap those if no keywords are used, while adding keywords would create an overhang again.

@kellerza
Copy link
Copy Markdown
Member

pylint has scope.... decorator would be preferred to the current disable so rather give that a go

For characteristics, I would suggest it so you can use it as follows, This is clear you are creating self.char_on:

    self.char_on = self._get_characteristic(
        serv_light, CHAR_ON, value=self._state, callback=self.set_state)

Maybe even add it to service.get_charactristic's call in the long run. Are you sure you are returning a copy, or you ok mutating its value and callback?

* HomeAccessory, HomeBridge: removed **kwargs
* Added defaults for a_type, config
@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 11, 2018

@kellerza I tested the decorator. Unfortunately it as well throws a pylint error as well: super-init-not-called. Maybe it shouldn't be.
The last variant I'm going to try is:

def __init__(self, *args, config):
    super().__init__(*args, category=CATEGORY_LIGHT)

Where *args = hass, name, entity_id, aid

@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 11, 2018

Regarding value and callback. It's a mutation / change of the original values. That's the intended way to use it, at least for the callback. When I think about it the value parameter can be remove entirely, since it is now covered by HAP-python to set the default values. They will be overridden anyway with the first call to update_state.


Update:

  • Value is necessary, since not all chars are automatically overridden.
  • I will try to get setup_char into the service class in HAP-python.

"""Initialize a Accessory object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
super().__init__(name, aid=aid)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you sure you never need config to be passed to the Accessory?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

About 90% sure that it isn't needed. The intention behind config was/is to use it for corner cases (e.g. the alarm_code) or to be able to define which type should be used there so it won't have to be in customize.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, because you used to pass it and stopped with the new code... otherwise all seems similar (hate to ask the obvious, but you did test it? 😄)

Copy link
Copy Markdown
Member Author

@cdce8p cdce8p Apr 11, 2018

Choose a reason for hiding this comment

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

Test what?
The config was never passed to super().__init__(), since they haven't been keyword arguments before, only normal ones. **kwargs was there to cover any case that I need to pass more arguments from get_accessory to the HAP-python accessory. However only used for aid.


Edit:
I did test all changes for lights at least. That works. The changes in the other types are mostly identical. I also change the same line in the tests. As you know with PR's that change the base structure it's almost impossible to test everything, but I'm quite confident that it won't introduce new errors.

Copy link
Copy Markdown
Member

@kellerza kellerza left a comment

Choose a reason for hiding this comment

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

One last question on config and then it should be ok.

It is much clearer & you removed ~60 lines, nice work! 🎉

@kellerza kellerza changed the title WIP: Style change follow up Style change follow up Apr 11, 2018
@kellerza kellerza merged commit 2a5751c into home-assistant:dev Apr 11, 2018
@cdce8p cdce8p deleted the homekit-static branch April 11, 2018 20:35
@cdce8p
Copy link
Copy Markdown
Member Author

cdce8p commented Apr 11, 2018

@kellerza Thanks for the help 👍

@cdce8p cdce8p mentioned this pull request Apr 12, 2018
4 tasks
@balloob balloob mentioned this pull request Apr 27, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Jul 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants