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

Add support for ZhongHong HVAC Controllers #14552

Merged
merged 24 commits into from
Jun 14, 2018

Conversation

crhan
Copy link
Contributor

@crhan crhan commented May 20, 2018

Description:

This is a platform for using a HVAC Controller product from Zhong hong tech which sells in China. It can control a bunch of AC brand like Daikin, Hitachi, Meidi, Gree, Haier, Toshiba and Mitsubishi. You can find the product selling page on taobao.com.

Related issue (if applicable): fixes #

Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#5402

Example entry for configuration.yaml (if applicable):

climate:
  - platform: zhong_hong
    host: 192.168.15.19

Checklist:

  • The code change is tested and works locally.
  • Local tests pass with tox. Your PR cannot be merged unless tests pass

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • New dependencies have been added to the REQUIREMENTS variable (example).
  • New dependencies are only imported inside functions that use them (example).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.

If the code does not interact with devices:

  • Tests have been added to verify that the new code works.

@crhan crhan changed the title [WIP]first blood for ZhongHong HVAC Controller first blood for ZhongHong HVAC Controller May 20, 2018
@fabaff fabaff changed the title first blood for ZhongHong HVAC Controller Add support for ZhongHong HVAC Controllers May 20, 2018
Copy link
Member

@fabaff fabaff left a comment

Choose a reason for hiding this comment

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

Just some quick comments to get started.


_LOGGER = logging.getLogger(__name__)

CONF_GW_ADDR = 'gw_addr'
Copy link
Member

Choose a reason for hiding this comment

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

Name it gateway_address. Will be easier to spot for humans.

host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
gw_addr = config.get(CONF_GW_ADDR)
hub = ZhongHongGateway(host, port, gw_addr)
Copy link
Member

Choose a reason for hiding this comment

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

I guess that there will be a traceback if the controller is not available. You need to decide what to do. Make the setup of the platform fails or raise PlatformNotReady.

for (addr_out, addr_in) in hub.discovery_ac()
]
add_devices(devices)
hub.start_listen()
Copy link
Member

Choose a reason for hiding this comment

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

Looks like that this should be attached to EVENT_HOMEASSISTANT_START

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why I have to put this after EVENT_HOMEASSISTANT_START? Just curious

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found some times this error will occur. What's wrong with it?

Exception in thread Thread-4:
Traceback (most recent call last):                                                                                                                                                                                                                                                File "/usr/local/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()                                                                                                                                                                                                                                                                    File "/usr/local/lib/python3.6/threading.py", line 864, in run                                                                                                                                                                                                                    self._target(*self._args, **self._kwargs)                                                                                                                                                                                                                                     File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hub.py", line 131, in _listen_to_msg                                                                                                                                                                               func(payload)                                                                                                                                                                                                                                                                 File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hvac.py", line 46, in _status_update                                                                                                                                                                               self._call_status_update()
  File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hvac.py", line 32, in _call_status_update
    func(self)
  File "/config/custom_components/climate/zhong_hong.py", line 74, in _after_update                                                                                                                                                                                                 self.schedule_update_ha_state()
  File "/usr/src/app/homeassistant/helpers/entity.py", line 294, in schedule_update_ha_state
    self.hass.add_job(self.async_update_ha_state(force_refresh))
AttributeError: 'NoneType' object has no attribute 'add_job'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we wait with adding the devices until home assistant start? @MartinHjelmare

Emmm, I don't know the reason. But I get the suggestion here.

"""Representation of a ZhongHong controller support HVAC."""

def __init__(self, hub, addr_out, addr_in):
"""Setup platform."""
Copy link
Member

Choose a reason for hiding this comment

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

This should be in imperative mood. Perhaps something like: "Set up the ZhongHong climate devices."

@property
def unique_id(self):
"""Return the unique ID of the HVAC."""
return "zhong_hong_hvac_%s_%s" % (self._device.addr_out,
Copy link
Member

Choose a reason for hiding this comment

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

Please use string formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there any further thought on using string format or % format?

@crhan
Copy link
Contributor Author

crhan commented May 21, 2018

BTW, what's the correct name for the mode of just blow the wind? Not making any cold or heat

@andersonshatch
Copy link
Contributor

Fan only is probably what you're looking for, there's a constant defined here for it
https://github.com/home-assistant/home-assistant/blob/fa9b9105a813285dce9cdb12638e83188285b3bb/homeassistant/components/climate/__init__.py#L45

@crhan
Copy link
Contributor Author

crhan commented May 27, 2018

thanks, FAN_ONLY name done.

Copy link
Contributor Author

@crhan crhan left a comment

Choose a reason for hiding this comment

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

still cannot fix this problem:

  File "/usr/local/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hub.py", line 148, in _listen_to_msg
    func(payload)
  File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hvac.py", line 50, in _status_update
    self._call_status_update()
  File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hvac.py", line 32, in _call_status_update
    func(self)
  File "/config/custom_components/climate/zhong_hong.py", line 74, in _after_update
    self.schedule_update_ha_state()
  File "/usr/src/app/homeassistant/helpers/entity.py", line 294, in schedule_update_ha_state
    self.hass.add_job(self.async_update_ha_state(force_refresh))
AttributeError: 'NoneType' object has no attribute 'add_job'

@property
def unique_id(self):
"""Return the unique ID of the HVAC."""
return "zhong_hong_hvac_%s_%s" % (self._device.addr_out,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there any further thought on using string format or % format?

for (addr_out, addr_in) in hub.discovery_ac()
]
add_devices(devices)
hub.start_listen()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found some times this error will occur. What's wrong with it?

Exception in thread Thread-4:
Traceback (most recent call last):                                                                                                                                                                                                                                                File "/usr/local/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()                                                                                                                                                                                                                                                                    File "/usr/local/lib/python3.6/threading.py", line 864, in run                                                                                                                                                                                                                    self._target(*self._args, **self._kwargs)                                                                                                                                                                                                                                     File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hub.py", line 131, in _listen_to_msg                                                                                                                                                                               func(payload)                                                                                                                                                                                                                                                                 File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hvac.py", line 46, in _status_update                                                                                                                                                                               self._call_status_update()
  File "/config/deps/lib/python3.6/site-packages/zhong_hong_hvac/hvac.py", line 32, in _call_status_update
    func(self)
  File "/config/custom_components/climate/zhong_hong.py", line 74, in _after_update                                                                                                                                                                                                 self.schedule_update_ha_state()
  File "/usr/src/app/homeassistant/helpers/entity.py", line 294, in schedule_update_ha_state
    self.hass.add_job(self.async_update_ha_state(force_refresh))
AttributeError: 'NoneType' object has no attribute 'add_job'

]
except Exception as exc:
_LOGGER.error("ZhongHong controller is not ready", exc_info=exc)
raise PlatformNotReady
Copy link
Member

Choose a reason for hiding this comment

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

This will not matter becaus you're no longer running this inside setup_platform. You need to test the credentials inside the setup_platform method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this the right way to solve the problem?

def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the ZhongHong HVAC platform."""
    from zhong_hong_hvac.hub import ZhongHongGateway
    host = config.get(CONF_HOST)
    port = config.get(CONF_PORT)
    gw_addr = config.get(CONF_GATEWAY_ADDRRESS)
    hub = ZhongHongGateway(host, port, gw_addr)
    try:
        devices = [
            ZhongHongClimate(hub, addr_out, addr_in)
            for (addr_out, addr_in) in hub.discovery_ac()
        ]
    except Exception as exc:
        _LOGGER.error("ZhongHong controller is not ready", exc_info=exc)
        raise PlatformNotReady

    def startup(event):
        """Add devices to HA and start hub socket."""
        add_devices(devices)
        hub.start_listen()
        hub.query_all_status()

    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup)

@property
def max_temp(self):
"""Return the maximum temperature."""
return convert_temperature(self._device.max_temp, TEMP_CELSIUS,
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to convert anything. Your device is already in CELSIUS and you are converting to CELSIUS. Just return self._device_max_temp

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@crhan
Copy link
Contributor Author

crhan commented Jun 3, 2018

@MartinHjelmare Have a look~

Copy link
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

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

Looks good. A small comment on a name and a question.

CONF_GATEWAY_ADDRRESS = 'gateway_address'

REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
SIGNAL_DEVICE_SETTED_UP = 'zhong_hong_device_setted_up'
Copy link
Member

Choose a reason for hiding this comment

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

SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added'

return

_LOGGER.debug("zhong_hong hub start listen event")
hub.start_listen()
Copy link
Member

Choose a reason for hiding this comment

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

Are these calls to the hub async safe? No I/O or blocking?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, It just start a thread to process the tcp stream. https://github.com/crhan/ZhongHongHVAC/blob/master/zhong_hong_hvac/hub.py#L159-L174

BTW, how can I change the TCP socket process code to async friendly?

Copy link
Member

Choose a reason for hiding this comment

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

There's a potential socket interaction and sleep call here. Let's do this in a worker thread via use of async_add_job.

Copy link
Member

Choose a reason for hiding this comment

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

If you want to build an async library, you would use either of the two alternatives for socket communication in asyncio standard library, transports and protocols or streams.

https://docs.python.org/3/library/asyncio-protocol.html

https://docs.python.org/3/library/asyncio-stream.html

SIGNAL_DEVICE_SETTED_UP -> SIGNAL_DEVICE_ADDED
Copy link
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

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

I think you should change back to setup_platform and the sync api but keep startup a coroutine. There's less context switching needed that way since the library is not async.

for (addr_out, addr_in) in hub.discovery_ac()
]

except Exception as exc:
Copy link
Member

Choose a reason for hiding this comment

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

Reading the library, it looks like you already catch Exception in the library when dealing with the socket. When could an exception bubble up to here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I will let the exception through out in the future. Must I remove the stupid try...except now?

Copy link
Member

Choose a reason for hiding this comment

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

You should never catch a bare exception. You should always only catch the exceptions that you expect.

Copy link
Member

Choose a reason for hiding this comment

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

So if you're using a Socket, see what the base class is and catch that.

Copy link
Member

Choose a reason for hiding this comment

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

This has to be updated to be the exception that is raised. You are not allowed to catch a bare exception

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry I missed this comment. It is really not necessary to catch this exception. Because I catch the exception in the device library.

return

_LOGGER.debug("zhong_hong hub start listen event")
hub.start_listen()
Copy link
Member

Choose a reason for hiding this comment

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

There's a potential socket interaction and sleep call here. Let's do this in a worker thread via use of async_add_job.


_LOGGER.debug("zhong_hong hub start listen event")
hub.start_listen()
hub.query_all_status()
Copy link
Member

Choose a reason for hiding this comment

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

This is sending messages to the device via socket and must be done in a worker thread (not inside event loop).

try:
devices = [
ZhongHongClimate(hub, addr_out, addr_in)
for (addr_out, addr_in) in hub.discovery_ac()
Copy link
Member

Choose a reason for hiding this comment

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

This is doing I/O via socket and is not async safe. I would change back to the sync api and use setup_platform. You can still have startup be a coroutine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can I still use async_add_job here to avoid the problem?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but I would instead change to setup_platform as mentioned.

@crhan
Copy link
Contributor Author

crhan commented Jun 5, 2018

Is there any code I need to change? The async def async_setup_platform has changed to def setup_platform already.


operation_mode = kwargs.get(ATTR_OPERATION_MODE)
if operation_mode is not None:
self._device.set_operation_mode(operation_mode)
Copy link
Member

Choose a reason for hiding this comment

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

Here you don't cast the operation mode to upper case like below. Is that not needed?

Copy link
Contributor Author

@crhan crhan Jun 7, 2018

Choose a reason for hiding this comment

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

Nope, you are right. I think I should use self.set_operation_mode directly, but not use self._device.set_operation_mode.

@ghost ghost added the in progress label Jun 7, 2018
]

except Exception as exc:
_LOGGER.error("ZhongHong controller is not ready", exc_info=exc)
Copy link
Member

Choose a reason for hiding this comment

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

Don't print the full stack trace of the exception, that will confuse users. Stringify the exception instead

self.is_initialized = True
async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADDED)

def update(self):
Copy link
Member

Choose a reason for hiding this comment

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

This is never called?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose home assistant core logic will invoke this method in some situation?

Copy link
Member

@MartinHjelmare MartinHjelmare Jun 10, 2018

Choose a reason for hiding this comment

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

Only if should_poll is true or if you pass True as second argument to add_devices or as first argument to schedule_update_ha_state.

]

except Exception as exc:
_LOGGER.error("ZhongHong controller is not ready: %s", str(exc))
Copy link
Member

Choose a reason for hiding this comment

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

Exceptions already have a magic string method. We don't need to copy to string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure

for (addr_out, addr_in) in hub.discovery_ac()
]

except Exception as exc:
Copy link
Member

Choose a reason for hiding this comment

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

This has to be updated to be the exception that is raised. You are not allowed to catch a bare exception

SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT,
EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS)
from homeassistant.exceptions import PlatformNotReady

Choose a reason for hiding this comment

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

'homeassistant.exceptions.PlatformNotReady' imported but unused

@balloob balloob merged commit c36c3f0 into home-assistant:dev Jun 14, 2018
@ghost ghost removed the in progress label Jun 14, 2018
@balloob balloob mentioned this pull request Jun 22, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Dec 10, 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.

7 participants