Add climate feature fan (#4)

* f1

* renamed to fan_mode

* fixes, add back compat with ha

* revert client_version - add swing
This commit is contained in:
Guillermo Ruffino 2019-11-16 12:34:14 -03:00 committed by Otto Winter
parent 9be72930e6
commit 511cb62dd9
7 changed files with 613 additions and 354 deletions

View File

@ -653,6 +653,25 @@ enum ClimateMode {
CLIMATE_MODE_AUTO = 1;
CLIMATE_MODE_COOL = 2;
CLIMATE_MODE_HEAT = 3;
CLIMATE_MODE_FAN_ONLY = 4;
CLIMATE_MODE_DRY = 5;
}
enum ClimateFanMode {
CLIMATE_FAN_ON = 0;
CLIMATE_FAN_OFF = 1;
CLIMATE_FAN_AUTO = 2;
CLIMATE_FAN_LOW = 3;
CLIMATE_FAN_MEDIUM = 4;
CLIMATE_FAN_HIGH = 5;
CLIMATE_FAN_MIDDLE = 6;
CLIMATE_FAN_FOCUS = 7;
CLIMATE_FAN_DIFFUSE = 8;
}
enum ClimateSwingMode {
CLIMATE_SWING_OFF = 0;
CLIMATE_SWING_BOTH = 1;
CLIMATE_SWING_VERTICAL = 2;
CLIMATE_SWINT_HORIZONTAL = 3;
}
enum ClimateAction {
CLIMATE_ACTION_OFF = 0;
@ -678,6 +697,8 @@ message ListEntitiesClimateResponse {
float visual_temperature_step = 10;
bool supports_away = 11;
bool supports_action = 12;
repeated ClimateFanMode supported_fan_modes = 13;
repeated ClimateSwingMode supported_swing_modes = 14;
}
message ClimateStateResponse {
option (id) = 47;
@ -693,6 +714,8 @@ message ClimateStateResponse {
float target_temperature_high = 6;
bool away = 7;
ClimateAction action = 8;
ClimateFanMode fan_mode = 9;
ClimateSwingMode swing_mode = 10;
}
message ClimateCommandRequest {
option (id) = 48;
@ -711,4 +734,8 @@ message ClimateCommandRequest {
float target_temperature_high = 9;
bool has_away = 10;
bool away = 11;
bool has_fan_mode = 12;
ClimateFanMode fan_mode = 13;
bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15;
}

View File

@ -1,30 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: api_options.proto
import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
from google.protobuf import symbol_database as _symbol_database
from google.protobuf import reflection as _reflection
from google.protobuf import message as _message
from google.protobuf import descriptor as _descriptor
from google.protobuf.internal import enum_type_wrapper
import sys
_b = sys.version_info[0] < 3 and (
lambda x: x) or (lambda x: x.encode('latin1'))
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
DESCRIPTOR = _descriptor.FileDescriptor(
name='api_options.proto',
package='',
syntax='proto2',
serialized_options=None,
serialized_pb=_b('\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto\"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse')
,
dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,])
serialized_pb=_b('\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto\"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse'),
dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR, ])
_APISOURCETYPE = _descriptor.EnumDescriptor(
name='APISourceType',
@ -149,20 +146,24 @@ DESCRIPTOR.extensions_by_name['log'] = log
DESCRIPTOR.extensions_by_name['no_delay'] = no_delay
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
void = _reflection.GeneratedProtocolMessageType('void', (_message.Message,), {
'DESCRIPTOR' : _VOID,
'__module__' : 'api_options_pb2'
void = _reflection.GeneratedProtocolMessageType('void', (_message.Message,), dict(
DESCRIPTOR=_VOID,
__module__='api_options_pb2'
# @@protoc_insertion_point(class_scope:void)
})
))
_sym_db.RegisterMessage(void)
google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_setup_connection)
google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_authentication)
google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(
needs_setup_connection)
google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(
needs_authentication)
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(id)
source.enum_type = _APISOURCETYPE
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(source)
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(
source)
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(ifdef)
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(log)
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(no_delay)
google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(
no_delay)
# @@protoc_insertion_point(module_scope)

File diff suppressed because one or more lines are too long

View File

@ -50,7 +50,8 @@ class APIClient:
raise
except Exception as e:
await _on_stop()
raise APIConnectionError("Unexpected error while connecting: {}".format(e))
raise APIConnectionError(
"Unexpected error while connecting: {}".format(e))
connected = True
@ -326,6 +327,8 @@ class APIClient:
target_temperature_low: Optional[float] = None,
target_temperature_high: Optional[float] = None,
away: Optional[bool] = None,
fan_mode: Optional[ClimateFanMode] = None,
swing_mode: Optional[ClimateSwingMode] = None,
) -> None:
self._check_authenticated()
@ -346,6 +349,12 @@ class APIClient:
if away is not None:
req.has_away = True
req.away = away
if fan_mode is not None:
req.has_fan_mode = True
req.fan_mode = fan_mode
if swing_mode is not None:
req.has_swing_mode = True
req.swing_mode = swing_mode
await self._connection.send_message(req)
async def execute_service(self, service: UserService, data: dict):
@ -357,7 +366,8 @@ class APIClient:
for arg_desc in service.args:
arg = pb.ExecuteServiceArgument()
val = data[arg_desc.name]
int_type = 'int_' if self.api_version >= APIVersion(1, 3) else 'legacy_int'
int_type = 'int_' if self.api_version >= APIVersion(
1, 3) else 'legacy_int'
map_single = {
UserServiceArgType.BOOL: 'bool_',
UserServiceArgType.INT: int_type,

View File

@ -120,10 +120,12 @@ class APIConnection:
await asyncio.wait_for(coro, 30.0)
except OSError as err:
await self._on_error()
raise APIConnectionError("Error connecting to {}: {}".format(sockaddr, err))
raise APIConnectionError(
"Error connecting to {}: {}".format(sockaddr, err))
except asyncio.TimeoutError:
await self._on_error()
raise APIConnectionError("Timeout while connecting to {}".format(sockaddr))
raise APIConnectionError(
"Timeout while connecting to {}".format(sockaddr))
_LOGGER.debug("%s: Opened socket for", self._params.address)
self._socket_reader, self._socket_writer = await asyncio.open_connection(sock=self._socket)
@ -140,7 +142,8 @@ class APIConnection:
_LOGGER.debug("%s: Successfully connected ('%s' API=%s.%s)",
self._params.address, resp.server_info, resp.api_version_major,
resp.api_version_minor)
self._api_version = APIVersion(resp.api_version_major, resp.api_version_minor)
self._api_version = APIVersion(
resp.api_version_major, resp.api_version_minor)
if self._api_version.major > 2:
_LOGGER.error("%s: Incompatible version %s! Closing connection",
self._api_version.major)
@ -187,7 +190,8 @@ class APIConnection:
await self._socket_writer.drain()
except OSError as err:
await self._on_error()
raise APIConnectionError("Error while writing data: {}".format(err))
raise APIConnectionError(
"Error while writing data: {}".format(err))
async def send_message(self, msg: message.Message) -> None:
for message_type, klass in MESSAGE_TYPE_TO_PROTO.items():
@ -197,7 +201,8 @@ class APIConnection:
raise ValueError
encoded = msg.SerializeToString()
_LOGGER.debug("%s: Sending %s: %s", self._params.address, type(msg), str(msg))
_LOGGER.debug("%s: Sending %s: %s",
self._params.address, type(msg), str(msg))
req = bytes([0])
req += _varuint_to_bytes(len(encoded))
req += _varuint_to_bytes(message_type)
@ -231,7 +236,8 @@ class APIConnection:
await asyncio.wait_for(fut, timeout)
except asyncio.TimeoutError:
if self._stopped:
raise APIConnectionError("Disconnected while waiting for API response!")
raise APIConnectionError(
"Disconnected while waiting for API response!")
raise APIConnectionError("Timeout while waiting for API response!")
try:
@ -250,7 +256,8 @@ class APIConnection:
res = await self.send_message_await_response_complex(
send_msg, is_response, is_response, timeout=timeout)
if len(res) != 1:
raise APIConnectionError("Expected one result, got {}".format(len(res)))
raise APIConnectionError(
"Expected one result, got {}".format(len(res)))
return res[0]
@ -261,7 +268,8 @@ class APIConnection:
try:
ret = await self._socket_reader.readexactly(amount)
except (asyncio.IncompleteReadError, OSError, TimeoutError) as err:
raise APIConnectionError("Error while receiving data: {}".format(err))
raise APIConnectionError(
"Error while receiving data: {}".format(err))
return ret
@ -281,7 +289,8 @@ class APIConnection:
raw_msg = await self._recv(length)
if msg_type not in MESSAGE_TYPE_TO_PROTO:
_LOGGER.debug("%s: Skipping message type %s", self._params.address, msg_type)
_LOGGER.debug("%s: Skipping message type %s",
self._params.address, msg_type)
return
msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
@ -289,7 +298,8 @@ class APIConnection:
msg.ParseFromString(raw_msg)
except Exception as e:
raise APIConnectionError("Invalid protobuf message: {}".format(e))
_LOGGER.debug("%s: Got message of type %s: %s", self._params.address, type(msg), msg)
_LOGGER.debug("%s: Got message of type %s: %s",
self._params.address, type(msg), msg)
for msg_handler in self._message_handlers[:]:
msg_handler(msg)
await self._handle_internal_messages(msg)

View File

@ -194,6 +194,27 @@ class ClimateMode(enum.IntEnum):
AUTO = 1
COOL = 2
HEAT = 3
FAN_ONLY = 4
DRY = 5
class ClimateFanMode(enum.IntEnum):
ON = 0
OFF = 1
AUTO = 2
LOW = 3
MEDIUM = 4
HIGH = 5
MIDDLE = 6
FOCUS = 7
DIFFUSE = 8
class ClimateSwingMode(enum.IntEnum):
OFF = 0
BOTH = 1
VERTICAL = 2
HORIZONTAL = 3
class ClimateAction(enum.IntEnum):
@ -206,6 +227,14 @@ def _convert_climate_modes(value):
return [ClimateMode(val) for val in value]
def _convert_climate_fan_modes(value):
return [ClimateFanMode(val) for val in value]
def _convert_climate_swing_modes(value):
return [ClimateSwingMode(val) for val in value]
@attr.s
class ClimateInfo(EntityInfo):
supports_current_temperature = attr.ib(type=bool, default=False)
@ -217,17 +246,31 @@ class ClimateInfo(EntityInfo):
visual_temperature_step = attr.ib(type=float, default=0.0)
supports_away = attr.ib(type=bool, default=False)
supports_action = attr.ib(type=bool, default=False)
supported_fan_modes = attr.ib(
type=List[ClimateFanMode], converter=_convert_climate_fan_modes, factory=list
)
supported_swing_modes = attr.ib(
type=List[ClimateSwingMode], converter=_convert_climate_swing_modes, factory=list
)
@attr.s
class ClimateState(EntityState):
mode = attr.ib(type=ClimateMode, converter=ClimateMode, default=ClimateMode.OFF)
action = attr.ib(type=ClimateAction, converter=ClimateAction, default=ClimateAction.OFF)
mode = attr.ib(type=ClimateMode, converter=ClimateMode,
default=ClimateMode.OFF)
action = attr.ib(type=ClimateAction, converter=ClimateAction,
default=ClimateAction.OFF)
current_temperature = attr.ib(type=float, default=0.0)
target_temperature = attr.ib(type=float, default=0.0)
target_temperature_low = attr.ib(type=float, default=0.0)
target_temperature_high = attr.ib(type=float, default=0.0)
away = attr.ib(type=bool, default=False)
fan_mode = attr.ib(
type=ClimateFanMode, converter=ClimateFanMode, default=ClimateFanMode.AUTO
)
swing_mode = attr.ib(
type=ClimateSwingMode, converter=ClimateSwingMode, default=ClimateSwingMode.OFF
)
COMPONENT_TYPE_TO_INFO = {

View File

@ -4,4 +4,4 @@
protoc --python_out=aioesphomeapi -I aioesphomeapi aioesphomeapi/*.proto
# https://github.com/protocolbuffers/protobuf/issues/1491
sed -i '' 's/import api_options_pb2 as api__options__pb2/from . import api_options_pb2 as api__options__pb2/' aioesphomeapi/api_pb2.py
sed -i 's/import api_options_pb2 as api__options__pb2/from . import api_options_pb2 as api__options__pb2/' aioesphomeapi/api_pb2.py