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

Added component named switcher_kis switcher water heater integration. #22325

Merged
Merged
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fbbbced
Added component named switcher_kis switcher water heater integration.
TomerFi Mar 23, 2019
5d2044f
Fixed conflicts.
TomerFi Apr 6, 2019
4820ba6
Updated requirements.
TomerFi Apr 6, 2019
17cef18
Added manifest.json file and updated CODEOWNERS.
TomerFi Apr 6, 2019
cb00473
Fixed requirements_all.txt.
TomerFi Apr 6, 2019
8aaa421
Better component tests.
TomerFi Apr 10, 2019
bb21856
Removed unnecessary parameter from fixture function.
TomerFi Apr 10, 2019
6ee2968
Removed tests section from mypy.ini.
TomerFi Apr 11, 2019
d0391d7
Remove unused ENTITY_ID_FORMAT.
TomerFi Apr 11, 2019
c9f28a5
Stop udp bridge when failed to setup the component.
TomerFi Apr 11, 2019
e70407e
Replace DISCOVERY_ constants prefix with DATA_.
TomerFi Apr 11, 2019
d65ed8f
Various change requests.
TomerFi Apr 11, 2019
0736cf1
Fixed constant name change remifications.
TomerFi Apr 11, 2019
26815c8
Added explicit name to fixture.
TomerFi Apr 11, 2019
d62f99f
Various change requests.
TomerFi Apr 11, 2019
38619e8
More various change requests.
TomerFi Apr 11, 2019
011dd16
Added EventType for homeassistant.core.Event.
TomerFi Apr 12, 2019
f778981
Switched from event driven data distribution to dispatcher type plus …
TomerFi Apr 12, 2019
6a50f3f
Removed name and icon keys from the component configuration.
TomerFi Apr 12, 2019
3495e05
Various change requests.
TomerFi Apr 13, 2019
0bd8b08
Various change reqeusts and clean-ups.
TomerFi Apr 13, 2019
e7b2330
Removed unnecessary DEPENDENCIES constant from swith platform.
TomerFi Apr 13, 2019
c4441c8
Replaced configuration data guard with assert.
TomerFi Apr 13, 2019
8babc45
Removed unused constants.
TomerFi Apr 13, 2019
de6a41f
Removed confusing type casting for mypy sake.
TomerFi Apr 13, 2019
105df9f
Refactor property device_name to name.
TomerFi Apr 13, 2019
b1c68d9
Removed None guard effecting mypy only.
TomerFi Apr 13, 2019
dccbe44
Removed unnecessary function from switch entity.
TomerFi Apr 13, 2019
6709f09
Removed None guard in use by mypy only.
TomerFi Apr 13, 2019
77458d1
Removed unused constant.
TomerFi Apr 13, 2019
5237b97
Removed unnecessary context manager.
TomerFi Apr 13, 2019
a53ee0d
Stopped messing around with mypy.ini.
TomerFi Apr 13, 2019
f3812d3
Referring to typing.TYPE_CHECKING for non-runtime imports.
TomerFi Apr 13, 2019
c18e742
Added test requierment correctyly.
TomerFi Apr 13, 2019
c518dc4
Replaced queue.get() with queue.get_nowait() to avoid backing up inte…
TomerFi Apr 13, 2019
cb7ab11
Revert changes in mypy.ini.
TomerFi Apr 14, 2019
02704fb
Changed attributes content to device properties instead of entity pro…
TomerFi Apr 15, 2019
f63e694
Fixed typo in constant name.
TomerFi Apr 15, 2019
5b30d84
Remove unnecessary async keyword from callable.
TomerFi Apr 15, 2019
0557f5a
Waiting for tasks on event loop to end.
TomerFi Apr 15, 2019
e26245e
Added callback decorator to callable.
TomerFi Apr 19, 2019
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -565,6 +565,7 @@ omit =
homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthru/sensor.py
homeassistant/components/synology/camera.py
@@ -199,6 +199,7 @@ homeassistant/components/sun/* @home-assistant/core
homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff
@@ -0,0 +1,93 @@
"""Home Assistant Switcher Component."""

from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for
from datetime import datetime, timedelta
from logging import getLogger
from typing import Dict, Optional

import voluptuous as vol

from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import EventType, HomeAssistantType

_LOGGER = getLogger(__name__)

DOMAIN = 'switcher_kis'

CONF_DEVICE_ID = 'device_id'
CONF_DEVICE_PASSWORD = 'device_password'
CONF_PHONE_ID = 'phone_id'

DATA_DEVICE = 'device'

SIGNAL_SWITCHER_DEVICE_UPDATE = 'switcher_device_update'

ATTR_AUTO_OFF_SET = 'auto_off_set'
ATTR_ELECTRIC_CURRENT = 'electric_current'
ATTR_REMAINING_TIME = 'remaining_time'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PHONE_ID): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Required(CONF_DEVICE_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
"""Set up the switcher component."""
from aioswitcher.bridge import SwitcherV2Bridge

phone_id = config[DOMAIN][CONF_PHONE_ID]
device_id = config[DOMAIN][CONF_DEVICE_ID]
device_password = config[DOMAIN][CONF_DEVICE_PASSWORD]

v2bridge = SwitcherV2Bridge(
hass.loop, phone_id, device_id, device_password)

await v2bridge.start()

async def async_stop_bridge(event: EventType) -> None:
"""On homeassistant stop, gracefully stop the bridge if running."""
await v2bridge.stop()

hass.async_add_job(hass.bus.async_listen_once(

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Apr 19, 2019

Member

We shouldn't use hass.async_add_job anymore. It's legacy.

Why do we need to add a job for this at all?

This comment has been minimized.

Copy link
@TomerFi

TomerFi Apr 26, 2019

Author Contributor

@MartinHjelmare
Much appreciated!
I addressed this and the other issues you referenced in a new PR I'm currently working on.
I'll notify you once I'll create the new pull request.

Thank you very much.
:-)

This comment has been minimized.

Copy link
@TomerFi

TomerFi Apr 27, 2019

Author Contributor

The new PR is here.
Besides resolving all the change requests from this PR, it also adds a couple of services and update the requirement version.
:-)

EVENT_HOMEASSISTANT_STOP, async_stop_bridge))

try:
device_data = await wait_for(
v2bridge.queue.get(), timeout=5.0, loop=hass.loop)
except (Asyncio_TimeoutError, RuntimeError):
_LOGGER.exception("failed to get response from device")

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Apr 19, 2019

Member

We start logging messages with capital letter.

await v2bridge.stop()
return False
This conversation was marked as resolved by andrewsayre

This comment has been minimized.

Copy link
@andrewsayre

andrewsayre Apr 11, 2019

Member

Stop the bridge here before you return - no need to keep it open if we've failed setup.

This comment has been minimized.

Copy link
@TomerFi

TomerFi Apr 13, 2019

Author Contributor

Fixed in this commit.


hass.data[DOMAIN] = {
DATA_DEVICE: device_data
}

hass.async_create_task(async_load_platform(
hass, SWITCH_DOMAIN, DOMAIN, None, config))

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Apr 19, 2019

Member

Pass en emtpy dict as discovery_info, ie fourth argument.


@callback
def device_updates(timestamp: Optional[datetime]) -> None:
This conversation was marked as resolved by TomerFi

This comment has been minimized.

Copy link
@andrewsayre

andrewsayre Apr 18, 2019

Member

You need to add the @callback decorator so that this doesn't run in the executor or change it back to async. You need this code to run in the loop because it's calling async_dispatcher_send.

This comment has been minimized.

Copy link
@TomerFi

TomerFi Apr 19, 2019

Author Contributor

Fixed in this commit.

"""Use for updating the device data from the queue."""
if v2bridge.running:
try:
device_new_data = v2bridge.queue.get_nowait()
if device_new_data:
async_dispatcher_send(
hass, SIGNAL_SWITCHER_DEVICE_UPDATE, device_new_data)
except QueueEmpty:
pass

async_track_time_interval(hass, device_updates, timedelta(seconds=4))

return True
@@ -0,0 +1,12 @@
{
"domain": "switcher_kis",
"name": "Switcher",
"documentation": "https://www.home-assistant.io/components/switcher_kis/",
"codeowners": [
"@tomerfi"
],
"requirements": [
"aioswitcher==2019.3.21"
],
"dependencies": []
}
@@ -0,0 +1,142 @@
"""Home Assistant Switcher Component Switch platform."""

from logging import getLogger
from typing import Callable, Dict, TYPE_CHECKING

from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType

from . import (
ATTR_AUTO_OFF_SET, ATTR_ELECTRIC_CURRENT, ATTR_REMAINING_TIME,
DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE)

if TYPE_CHECKING:
from aioswitcher.devices import SwitcherV2Device
from aioswitcher.api.messages import SwitcherV2ControlResponseMSG


_LOGGER = getLogger(__name__)

DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = {
'power_consumption': ATTR_CURRENT_POWER_W,
'electric_current': ATTR_ELECTRIC_CURRENT,
'remaining_time': ATTR_REMAINING_TIME,
'auto_off_set': ATTR_AUTO_OFF_SET
}


async def async_setup_platform(hass: HomeAssistantType, config: Dict,
async_add_entities: Callable,
discovery_info: Dict) -> None:
"""Set up the switcher platform for the switch component."""
assert DOMAIN in hass.data

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Apr 19, 2019

Member

Instead check if discovery_info is None and return if so.

async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])])


class SwitcherControl(SwitchDevice):
"""Home Assistant switch entity."""

def __init__(self, device_data: 'SwitcherV2Device') -> None:
"""Initialize the entity."""
self._self_initiated = False
self._device_data = device_data
self._state = device_data.state

@property
def name(self) -> str:
"""Return the device's name."""
return self._device_data.name

@property
def should_poll(self) -> bool:
"""Return False, entity pushes its state to HA."""
return False

@property
def unique_id(self) -> str:
"""Return a unique ID."""
return "{}-{}".format(
self._device_data.device_id, self._device_data.mac_addr)

@property
def is_on(self) -> bool:
"""Return True if entity is on."""
from aioswitcher.consts import STATE_ON as SWITCHER_STATE_ON
return self._state == SWITCHER_STATE_ON

@property
def current_power_w(self) -> int:
"""Return the current power usage in W."""
return self._device_data.power_consumption

@property
def device_state_attributes(self) -> Dict:
"""Return the optional state attributes."""
from aioswitcher.consts import WAITING_TEXT

attribs = {}

for prop, attr in DEVICE_PROPERTIES_TO_HA_ATTRIBUTES.items():
value = getattr(self._device_data, prop)
if value and value is not WAITING_TEXT:
attribs[attr] = value

return attribs

@property
def available(self) -> bool:
"""Return True if entity is available."""
from aioswitcher.consts import (STATE_OFF as SWITCHER_STATE_OFF,
STATE_ON as SWITCHER_STATE_ON)
return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF]

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
async_dispatcher_connect(
self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data)

async def async_update_data(self, device_data: 'SwitcherV2Device') -> None:
"""Update the entity data."""
if device_data:
if self._self_initiated:
This conversation was marked as resolved by andrewsayre

This comment has been minimized.

Copy link
@awarecan

awarecan Apr 13, 2019

Contributor

I have a little confuse about this, is _self_initiated really means as it's name?

This comment has been minimized.

Copy link
@TomerFi

TomerFi Apr 13, 2019

Author Contributor

@awarecan
First of all, thank you for your much appreciated review, I've already started working on your change requests.

In regards to your question:
The data updates from the device comes via a UDP connection approximately every 4 seconds.
The commands send to device, for example turn on/off are manual TCP requests.

When a TCP request for turning on the device is sent just before or whilst the UDP data update is being transmitted, we end up with a false outcome because according to the data update, the device is off but in HA it just been marked on.
The result is the entity being turned on and almost immediately being turned off (although the device is actually turned on) and in about 4 seconds it'll get the correct data update and get turned on again.

To resolve this, the _self_initiated==True marks the the turn on/off request was sent by HA entity, in which case the update process will skip the current data update and re-set _self_initiated=False so that the next update go on correctly.

Maybe the name _self_initiated is confusing.
Would you recommend changing it perhaps?

This comment has been minimized.

Copy link
@andrewsayre

andrewsayre Apr 14, 2019

Member

I initially read it as initialized -- wonder if you did too @awarecan. I think initiated is fine in this case. Ideally, this would be logic handled by the library -- not inside HASS, but you can address that in the future.

This comment has been minimized.

Copy link
@awarecan

awarecan Apr 14, 2019

Contributor

@andrewsayre, you are exactly right, I have read it as initialized 😄

self._self_initiated = False
else:
self._device_data = device_data
self._state = self._device_data.state
self.async_schedule_update_ha_state()

async def async_turn_on(self, **kwargs: Dict) -> None:
"""Turn the entity on.
This method must be run in the event loop and returns a coroutine.
"""
await self._control_device(True)

async def async_turn_off(self, **kwargs: Dict) -> None:
This conversation was marked as resolved by TomerFi

This comment has been minimized.

Copy link
@andrewsayre

andrewsayre Apr 11, 2019

Member

These methods only really differ by a couple lines -- could you create a shared function instead to eliminate the repetitive code?

This comment has been minimized.

Copy link
@TomerFi

TomerFi Apr 13, 2019

Author Contributor

Fixed in this commit.

"""Turn the entity off.
This method must be run in the event loop and returns a coroutine.
"""
await self._control_device(False)

async def _control_device(self, send_on: bool) -> None:
"""Turn the entity on or off."""
from aioswitcher.api import SwitcherV2Api
from aioswitcher.consts import (COMMAND_OFF, COMMAND_ON,
STATE_OFF as SWITCHER_STATE_OFF,
STATE_ON as SWITCHER_STATE_ON)

response = None # type: SwitcherV2ControlResponseMSG
async with SwitcherV2Api(
self.hass.loop, self._device_data.ip_addr,
self._device_data.phone_id, self._device_data.device_id,
self._device_data.device_password) as swapi:
response = await swapi.control_device(
COMMAND_ON if send_on else COMMAND_OFF)

if response and response.successful:
self._self_initiated = True
self._state = \
SWITCHER_STATE_ON if send_on else SWITCHER_STATE_OFF
self.async_schedule_update_ha_state()
@@ -7,6 +7,7 @@

GPSType = Tuple[float, float]
ConfigType = Dict[str, Any]
EventType = homeassistant.core.Event
HomeAssistantType = homeassistant.core.HomeAssistant
ServiceDataType = Dict[str, Any]
TemplateVarsType = Optional[Dict[str, Any]]
@@ -142,6 +142,9 @@ aiolifx_effects==0.2.1
# homeassistant.components.hunterdouglas_powerview
aiopvapi==1.6.14

# homeassistant.components.switcher_kis
aioswitcher==2019.3.21

# homeassistant.components.unifi
aiounifi==4

@@ -51,6 +51,9 @@ aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==1.9.1

# homeassistant.components.switcher_kis
aioswitcher==2019.3.21

# homeassistant.components.unifi
aiounifi==4

@@ -48,6 +48,7 @@
'aiohttp_cors',
'aiohue',
'aiounifi',
'aioswitcher',
'apns2',
'av',
'axis',
@@ -0,0 +1 @@
"""Test cases and object for the Switcher integration tests."""
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.