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 integration for Vallox Ventilation Units #24660

Merged
merged 5 commits into from Jun 25, 2019

Conversation

@andre-richter
Copy link
Contributor

commented Jun 20, 2019

Description:

This PR adds initial support for Vallox ventilation units that are compatible to the vallox_websocket_api PyPi library.

The fan component code reuses parts from the Xiaomi miio fan code, which has been used as a template for this integration.

Related issue (if applicable): N/A

Pull request with documentation for home-assistant.io (if applicable): home-assistant/home-assistant.io#9668

Example entry for configuration.yaml (if applicable):

vallox:
  host: IP_ADDRESS

Checklist:

  • The code change is tested and works locally.
  • Local tests pass with tox. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist

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

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

  • The manifest file has all fields filled out correctly. Update and include derived files by running python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt by running python3 -m script.gen_requirements_all.
  • Untested files have been added to .coveragerc.

If the code does not interact with devices:

  • Tests have been added to verify that the new code works.
@andre-richter

This comment has been minimized.

Copy link
Contributor Author

commented Jun 20, 2019

@yozik04 FYI.

@andre-richter andre-richter force-pushed the andre-richter:vallox branch from c1a9804 to b569c4e Jun 20, 2019

@yozik04

This comment has been minimized.

Copy link

commented Jun 20, 2019

Ohh. Nice!

@andre-richter andre-richter force-pushed the andre-richter:vallox branch from b569c4e to 7ce5051 Jun 20, 2019

SPEED_BOOST = 'Boost'
SPEED_FIREPLACE = 'Fireplace'

SPEED_LIST = [SPEED_HOME, SPEED_AWAY, SPEED_BOOST, SPEED_FIREPLACE]

This comment has been minimized.

Copy link
@yozik04

yozik04 Jun 21, 2019

Why you mess names? It makes hard to understand. SPEED != PROFILE. I would rename all SPEED_* -> PROFILE_*

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 21, 2019

Author Contributor

I did it because the HA functions consuming it later have "speed" in their name. So one way or the other we'll have this indirection.

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Only speeds in the base fan component are allowed for the set speed service/method and speed property.

SPEED_OFF = 'off'
SPEED_LOW = 'low'
SPEED_MEDIUM = 'medium'
SPEED_HIGH = 'high'

See this architecture issue for a future possible change:
home-assistant/architecture#27

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 22, 2019

Author Contributor

If it is a hard requirement to only expose these speed options, then I fear this is a blocker for the Vallox ventilation unit integration as I picture it. The units are always-on units that constantly supply a whole building with fresh air, and the control interfaces expose four inherent, manufacturer-provided profiles between which the user can switch. Here's a screenshot of OEM web client:

image

Controlling your Vallox units from within HA only makes sense if the profile semantics can be kept. Can we make an exception until your linked PR lands?

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

It's a hard requirement. We are free to map the device modes to the home assistant speed modes, but that might not make sense in all cases.

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 22, 2019

Author Contributor

While I find it a bit stubborn to not allow this temporarily, I will transform profile selection to a service then.
Give me some days to complete this.

@yozik04

This comment has been minimized.

Copy link

commented Jun 21, 2019

I need to make my lib async as well...

@andre-richter andre-richter force-pushed the andre-richter:vallox branch from a17bb4a to 6f4113e Jun 21, 2019

@MartinHjelmare
Copy link
Member

left a comment

I've not done a full review yet. I've only commented on the main architectural dependent changes needed.

@@ -211,3 +211,34 @@ wemo_reset_filter_life:
entity_id:
description: Names of the WeMo humidifier entities (1 or more entity_ids are required).
example: 'fan.wemo_humidifier'

vallox_set_profile_fan_speed_home:

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Integration specific services should be added to the integration package and registered under the integration domain.

Move these service descriptions to homeassistant/components/vallox/services.yaml and change the registration to the mentioned domain.

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 22, 2019

Author Contributor

Will change.
Just for my own understanding: I tried to start from an existing integration, in this case xiaomi miio and wemo, where they did it that way. Was I wrong to assume this is the same case, or did these rules not apply back when they integrated it?

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Those integrations are not the best examples, since they don't always follow our current standards and architecture.

Our architecture decisions have evolved and there are still some old integrations that don't follow them.

"Check for \"vallox:\" in the configuration.")
return

global SENSOR_INSTANCES

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Don't use global. We can use hass.data to store instances specific to the integration platform.

hass.data[DATA_VALLOX_SENSOR_LIST] = sensor_list
await discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)

await discovery.async_load_platform(hass, 'fan', DOMAIN, {}, config)

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Use hass.async_create_task to schedule this as a task to avoid possible deadlock.

DOMAIN: vol.Schema({
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]),

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

We no longer want to include config options for selecting sensor types. All available types should be included by default.

See https://github.com/home-assistant/architecture/blob/master/adr/0003-monitor-condition-and-data-selectors.md.

"requirements": [
"vallox-websocket-api==1.5.1",
"numpy==1.16.3",
"websocket-client==0.54.0"

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Why do we need to include the websocket client as a requirement? I would think that the vallox websocket api library would depend on the websocket client library.

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 22, 2019

Member

Why include numpy? We don't seem to use that.

This comment has been minimized.

Copy link
@yozik04

yozik04 Jun 22, 2019

vallox-websockets-api lib uses that. But it should be pulled as lib dependency then.

@MartinHjelmare MartinHjelmare changed the title Add integration for Vallox Ventilation Units. Add integration for Vallox Ventilation Units Jun 22, 2019

@yozik04

This comment has been minimized.

Copy link

commented Jun 22, 2019

I am in progress or changing vallox-websocket-api to be async. Maybe will finish next week.

@andre-richter

This comment has been minimized.

Copy link
Contributor Author

commented Jun 22, 2019

I want to refactor a bit and introduce an intermediary class that fetches and caches all read values with a single call periodically. Fan and sensor classes would then fetch those cached values.
This would reduce the current wasteful number of websocket calls.

@MartinHjelmare, which is an integration I can use as reference? Is smarty okay?

@MartinHjelmare

This comment has been minimized.

Copy link
Member

commented Jun 22, 2019

Yes, that's a good example. 👍

@andre-richter andre-richter force-pushed the andre-richter:vallox branch from 6f4113e to ae8c6e1 Jun 23, 2019

@andre-richter

This comment has been minimized.

Copy link
Contributor Author

commented Jun 23, 2019

Addressing the review comments resulted in a bigger refactoring. Please have a look. Might still need some tweaks here and there, but I hope I addressed all the bigger architectural concerns.

Removing numpy and websocket-client from the manifest did not work in my tests. They were not pulled in automatically when executing hass for the first time. Instead, an error message stated that loading the vallox integration did not work because those packets are missing.
Please advise on that part.

I will update the corresponding documentation PR once we agreed on the architecture.

@andre-richter andre-richter force-pushed the andre-richter:vallox branch from ae8c6e1 to 6440f8a Jun 23, 2019

self._profile = None
self._valid = False

async_track_time_interval(self._hass, self.async_update, SCAN_INTERVAL)

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

We don't want side effects in init method. Please move this outside the class or into a dedicated setup method.


def get_profile(self):
"""Return cached profile value."""
_LOGGER.debug("Returning profile.")

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

We don't end logging messages with period.

_LOGGER.debug("Updating Vallox state cache.")

try:
self._metric_cache = self._client.fetch_metrics()

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

If I'm reading the vallox library correctly, this does non asyncio I/O and we're in a coroutine here. That's not allowed. Coroutines will be executed in the event loop which should never block.

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

Consider switching affected methods to regular sync functions. This might mean we have to use the home assistant sync api.

The alternative is to schedule I/O on the executor thread pool with hass.async_add_executor_job. But if we need to do that a lot, the sync api is cleaner.

device = ValloxFan(hass.data[DOMAIN]['name'],
hass.data[DOMAIN]['client'],
hass.data[DOMAIN]['state_proxy'])
except KeyError:

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

Remove this. Instead add a guard clause at the top of the function body that checks if discovery_info is None and return if so.


async def async_setup(hass, config):
"""Set up the client and boot the platforms."""
if DOMAIN not in config:

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

This can't happen. There is no network discovery and we're not using config entries here yet.

self._fan_speed_boost = None

try:
self._client.set_settable_address(METRIC_KEY_MODE, int)

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

Please avoid side effects in init method.

self._available = False
_LOGGER.error("Error turning on: %s", io_err)
else:
_LOGGER.error("Already on.")

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

If we have the wrong state info we won't be able to change state. If that's a possibility, we try to avoid guards like this.

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 24, 2019

Author Contributor

I put it there to catch a call to the service fan/turn_on, where tests showed it executes even if the fan is already on.
This could for example happen if a user-automation script is erroneous.

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 24, 2019

Member

Would it be bad for the device to call turn on if it's already on?

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 24, 2019

Author Contributor

I don't know. There's no documentation that would give it away, so I did not want to take chances and cause side effects in the device when we can prevent it on the higher level.

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 24, 2019

Member

I think it's better to put protection close to the device if the device requires protection.

This comment has been minimized.

Copy link
@andre-richter

andre-richter Jun 24, 2019

Author Contributor

The problem is we cannot find out if the device firmware is fine with mutliple calls or if there would be unforeseen consequences if it would be spammed with those commands due to a user script gone rogue (making up a "dramatic" case here).

In the end it is just an if/else on a boolean to be on the safe side here. IMO, that's an acceptable price to pay for a little more safety?

_LOGGER.debug("Turn on: %s", speed)

# Case speed == None equals the GUI toggle switch being activated.
if speed is None:

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

Invert this and make it a guard clause instead. Ie return if true. Then we can outdent below.

if mode == 0:
self._state = True
else:
self._state = None

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 23, 2019

Member

We don't want to set it to False which means off?

@MartinHjelmare

This comment has been minimized.

Copy link
Member

commented Jun 23, 2019

I'll look into the requirements.

@MartinHjelmare

This comment has been minimized.

Copy link
Member

commented Jun 23, 2019

I think the design is ok. Consider using the home assistant sync api until the library supports asyncio.

@andre-richter

This comment has been minimized.

Copy link
Contributor Author

commented Jun 24, 2019

Due to lack of personal bandwidth, I went for executor_jobs instead of of changing APIs. This will also make the transition to the async version of vallox_websocket_api in the near future straight forward.

@MartinHjelmare
Copy link
Member

left a comment

I think this looks good! I'll have a look at the requirements now.

self._client.get_profile)
self._valid = True

except IOError as io_err:

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 24, 2019

Member

We can except OSError instead. IOError is an alias of OSError.

https://docs.python.org/3/library/exceptions.html#IOError

The following exceptions are kept for compatibility with previous versions; starting from Python 3.3, they are aliases of OSError.

Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.3.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: try: 
   ...:     raise IOError 
   ...: except OSError: 
   ...:     print("Caught the IOError") 
   ...:                                                                                                                                                                                                       
Caught the IOError

In [2]:
@MartinHjelmare

This comment has been minimized.

Copy link
Member

commented Jun 24, 2019

I think I've found the problem with the requirements not installing:
yozik04/vallox_websocket_api#9

@andre-richter andre-richter force-pushed the andre-richter:vallox branch from 1d7b93c to fd147c5 Jun 25, 2019

@yozik04

This comment has been minimized.

Copy link

commented Jun 25, 2019

PR Merged and new version created: https://pypi.org/project/vallox-websocket-api/
Use: 1.5.2

About async version of the API. For some reason aiohttp.ClientRequest does not connect to Vallox websocket... Wireshark shows exactly the same request. Response from the unit is different... Investigating.

@andre-richter

This comment has been minimized.

Copy link
Contributor Author

commented Jun 25, 2019

Tested and working with vallox_websocket_api 1.5.2

@MartinHjelmare
Copy link
Member

left a comment

Great!

@MartinHjelmare MartinHjelmare merged commit 236820d into home-assistant:dev Jun 25, 2019

13 checks passed

build Workflow: build
Details
ci/circleci: pre-install-all-requirements Your tests passed on CircleCI!
Details
ci/circleci: pre-test 3.5.5 Your tests passed on CircleCI!
Details
ci/circleci: pre-test 3.6 Your tests passed on CircleCI!
Details
ci/circleci: pre-test 3.7 Your tests passed on CircleCI!
Details
ci/circleci: pylint Your tests passed on CircleCI!
Details
ci/circleci: static-check Your tests passed on CircleCI!
Details
ci/circleci: test 3.5.5 Your tests passed on CircleCI!
Details
ci/circleci: test 3.6 Your tests passed on CircleCI!
Details
ci/circleci: test 3.7 Your tests passed on CircleCI!
Details
cla-bot Everyone involved has signed the CLA
codecov/patch Coverage not affected when comparing 9813396...91f1c9d
Details
codecov/project 94.14% (target 90%)
Details
@yozik04

This comment has been minimized.

Copy link

commented Jun 26, 2019

I have released async version of the API
vallox_websocket_api 2.0.0

Interface is exactly the same. Just add await to all called methods.

@balloob balloob referenced this pull request Jul 17, 2019
@andre-richter

This comment has been minimized.

Copy link
Contributor Author

commented Jul 19, 2019

Hi @yozik04 ,

Please see the error message below from https://community.home-assistant.io/t/vallox-valloplus-350mv-partial-success-work-in-progress-on-vallox-component/52588/30

Is the response from the Vallox unit bad? Anything we can do about it? I hit this bug as well already, sometimes it helps to try multiple times.

Error during setup of component vallox
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/websockets/client.py", line 77, in read_http_response
    status_code, headers = yield from read_response(self.reader)
  File "/usr/local/lib/python3.7/site-packages/websockets/http.py", line 123, in read_response
    status_line = yield from read_line(stream)
  File "/usr/local/lib/python3.7/site-packages/websockets/http.py", line 197, in read_line
    raise ValueError("Line without CRLF")
ValueError: Line without CRLF

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/setup.py", line 153, in _async_setup_component
    hass, processed_config)
  File "/usr/src/homeassistant/homeassistant/components/vallox/__init__.py", line 114, in async_setup
    await state_proxy.async_update(None)
  File "/usr/src/homeassistant/homeassistant/components/vallox/__init__.py", line 163, in async_update
    self._metric_cache = await self._client.fetch_metrics()
  File "/usr/local/lib/python3.7/site-packages/vallox_websocket_api/client.py", line 235, in fetch_metrics
    result = await self._websocket_request(command=vlxDevConstants.WS_WEB_UI_COMMAND_READ_TABLES)
  File "/usr/local/lib/python3.7/site-packages/vallox_websocket_api/client.py", line 223, in _websocket_request
    async with websockets.connect("ws://%s/" % self.ip_address) as ws:
  File "/usr/local/lib/python3.7/site-packages/websockets/py35/client.py", line 2, in __aenter__
    return await self
  File "/usr/local/lib/python3.7/site-packages/websockets/py35/client.py", line 19, in __await_impl__
    extra_headers=protocol.extra_headers,
  File "/usr/local/lib/python3.7/site-packages/websockets/client.py", line 260, in handshake
    status_code, response_headers = yield from self.read_http_response()
  File "/usr/local/lib/python3.7/site-packages/websockets/client.py", line 79, in read_http_response
    raise InvalidMessage("Malformed HTTP message") from exc
websockets.exceptions.InvalidMessage: Malformed HTTP message
@MartinHjelmare

This comment has been minimized.

Copy link
Member

commented Jul 20, 2019

Please don't discuss in merged PRs. Open an issue instead. Thanks!

@home-assistant home-assistant locked as resolved and limited conversation to collaborators Jul 20, 2019

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants
You can’t perform that action at this time.