-
-
Notifications
You must be signed in to change notification settings - Fork 32k
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
Conversation
There was a problem hiding this 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' |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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.""" |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use string formatting.
There was a problem hiding this comment.
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?
BTW, what's the correct name for the mode of just blow the wind? Not making any cold or heat |
Fan only is probably what you're looking for, there's a constant defined here for it |
thanks, FAN_ONLY name done. |
There was a problem hiding this 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, |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
@MartinHjelmare Have a look~ |
There was a problem hiding this 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' |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
SIGNAL_DEVICE_SETTED_UP -> SIGNAL_DEVICE_ADDED
There was a problem hiding this 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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Is there any code I need to change? The |
|
||
operation_mode = kwargs.get(ATTR_OPERATION_MODE) | ||
if operation_mode is not None: | ||
self._device.set_operation_mode(operation_mode) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
] | ||
|
||
except Exception as exc: | ||
_LOGGER.error("ZhongHong controller is not ready", exc_info=exc) |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is never called?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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):Checklist:
tox
. Your PR cannot be merged unless tests passIf user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
REQUIREMENTS
variable (example).requirements_all.txt
by runningscript/gen_requirements_all.py
..coveragerc
.If the code does not interact with devices: