mirror of
https://github.com/esphome/aioesphomeapi.git
synced 2024-09-27 04:22:46 +02:00
Big Refactor
This commit is contained in:
parent
6789949b99
commit
e74118923e
@ -1 +1,5 @@
|
|||||||
from .client import *
|
from .client import *
|
||||||
|
from .connection import *
|
||||||
|
from .core import *
|
||||||
|
from .model import *
|
||||||
|
from .util import *
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== BASE PACKETS ====================
|
||||||
|
|
||||||
// The Home Assistant protocol is structured as a simple
|
// The Home Assistant protocol is structured as a simple
|
||||||
// TCP socket with short binary messages encoded in the protocol buffers format
|
// TCP socket with short binary messages encoded in the protocol buffers format
|
||||||
// First, a message in this protocol has a specific format:
|
// First, a message in this protocol has a specific format:
|
||||||
@ -18,6 +21,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
// Message sent at the beginning of each connection
|
// Message sent at the beginning of each connection
|
||||||
// Can only be sent by the client and only at the beginning of the connection
|
// Can only be sent by the client and only at the beginning of the connection
|
||||||
|
// ID: 1
|
||||||
message HelloRequest {
|
message HelloRequest {
|
||||||
// Description of client (like User Agent)
|
// Description of client (like User Agent)
|
||||||
// For example "Home Assistant"
|
// For example "Home Assistant"
|
||||||
@ -28,6 +32,7 @@ message HelloRequest {
|
|||||||
|
|
||||||
// Confirmation of successful connection request.
|
// Confirmation of successful connection request.
|
||||||
// Can only be sent by the server and only at the beginning of the connection
|
// Can only be sent by the server and only at the beginning of the connection
|
||||||
|
// ID: 2
|
||||||
message HelloResponse {
|
message HelloResponse {
|
||||||
// The version of the API to use. The _client_ (for example Home Assistant) needs to check
|
// The version of the API to use. The _client_ (for example Home Assistant) needs to check
|
||||||
// for compatibility and if necessary adopt to an older API.
|
// for compatibility and if necessary adopt to an older API.
|
||||||
@ -44,6 +49,7 @@ message HelloResponse {
|
|||||||
|
|
||||||
// Message sent at the beginning of each connection to authenticate the client
|
// Message sent at the beginning of each connection to authenticate the client
|
||||||
// Can only be sent by the client and only at the beginning of the connection
|
// Can only be sent by the client and only at the beginning of the connection
|
||||||
|
// ID: 3
|
||||||
message ConnectRequest {
|
message ConnectRequest {
|
||||||
// The password to log in with
|
// The password to log in with
|
||||||
string password = 1;
|
string password = 1;
|
||||||
@ -51,33 +57,40 @@ message ConnectRequest {
|
|||||||
|
|
||||||
// Confirmation of successful connection. After this the connection is available for all traffic.
|
// Confirmation of successful connection. After this the connection is available for all traffic.
|
||||||
// Can only be sent by the server and only at the beginning of the connection
|
// Can only be sent by the server and only at the beginning of the connection
|
||||||
|
// ID: 4
|
||||||
message ConnectResponse {
|
message ConnectResponse {
|
||||||
bool invalid_password = 1;
|
bool invalid_password = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request to close the connection.
|
// Request to close the connection.
|
||||||
// Can be sent by both the client and server
|
// Can be sent by both the client and server
|
||||||
|
// ID: 5
|
||||||
message DisconnectRequest {
|
message DisconnectRequest {
|
||||||
// Do not close the connection before the acknowledgement arrives
|
// Do not close the connection before the acknowledgement arrives
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 6
|
||||||
message DisconnectResponse {
|
message DisconnectResponse {
|
||||||
// Empty - Both parties are required to close the connection after this
|
// Empty - Both parties are required to close the connection after this
|
||||||
// message has been received.
|
// message has been received.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 7
|
||||||
message PingRequest {
|
message PingRequest {
|
||||||
// Empty
|
// Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 8
|
||||||
message PingResponse {
|
message PingResponse {
|
||||||
// Empty
|
// Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 9
|
||||||
message DeviceInfoRequest {
|
message DeviceInfoRequest {
|
||||||
// Empty
|
// Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 10
|
||||||
message DeviceInfoResponse {
|
message DeviceInfoResponse {
|
||||||
bool uses_password = 1;
|
bool uses_password = 1;
|
||||||
|
|
||||||
@ -101,10 +114,21 @@ message DeviceInfoResponse {
|
|||||||
bool has_deep_sleep = 7;
|
bool has_deep_sleep = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 11
|
||||||
message ListEntitiesRequest {
|
message ListEntitiesRequest {
|
||||||
// Empty
|
// Empty
|
||||||
}
|
}
|
||||||
|
// ID: 19
|
||||||
|
message ListEntitiesDoneResponse {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
// ID: 20
|
||||||
|
message SubscribeStatesRequest {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== BINARY SENSOR ====================
|
||||||
|
// ID: 12
|
||||||
message ListEntitiesBinarySensorResponse {
|
message ListEntitiesBinarySensorResponse {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
@ -114,14 +138,69 @@ message ListEntitiesBinarySensorResponse {
|
|||||||
string device_class = 5;
|
string device_class = 5;
|
||||||
bool is_status_binary_sensor = 6;
|
bool is_status_binary_sensor = 6;
|
||||||
}
|
}
|
||||||
|
// ID: 21
|
||||||
|
message BinarySensorStateResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
bool state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== COVER ====================
|
||||||
|
// ID: 13
|
||||||
message ListEntitiesCoverResponse {
|
message ListEntitiesCoverResponse {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
string name = 3;
|
string name = 3;
|
||||||
string unique_id = 4;
|
string unique_id = 4;
|
||||||
|
|
||||||
bool is_optimistic = 5;
|
bool assumed_state = 5;
|
||||||
|
bool supports_position = 6;
|
||||||
|
bool supports_tilt = 7;
|
||||||
|
string device_class = 8;
|
||||||
}
|
}
|
||||||
|
// ID: 22
|
||||||
|
message CoverStateResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
|
||||||
|
// legacy: state has been removed in 1.13
|
||||||
|
// clients/servers must still send/accept it until the next protocol change
|
||||||
|
enum LegacyCoverState {
|
||||||
|
OPEN = 0;
|
||||||
|
CLOSED = 1;
|
||||||
|
}
|
||||||
|
LegacyCoverState legacy_state = 2;
|
||||||
|
|
||||||
|
float position = 3;
|
||||||
|
float tilt = 4;
|
||||||
|
enum CoverOperation {
|
||||||
|
IDLE = 0;
|
||||||
|
IS_OPENING = 1;
|
||||||
|
IS_CLOSING = 2;
|
||||||
|
}
|
||||||
|
CoverOperation current_operation = 5;
|
||||||
|
}
|
||||||
|
// ID: 30
|
||||||
|
message CoverCommandRequest {
|
||||||
|
fixed32 key = 1;
|
||||||
|
|
||||||
|
// legacy: command has been removed in 1.13
|
||||||
|
// clients/servers must still send/accept it until the next protocol change
|
||||||
|
enum LegacyCoverCommand {
|
||||||
|
OPEN = 0;
|
||||||
|
CLOSE = 1;
|
||||||
|
STOP = 2;
|
||||||
|
}
|
||||||
|
bool has_legacy_command = 2;
|
||||||
|
LegacyCoverCommand legacy_command = 3;
|
||||||
|
|
||||||
|
bool has_position = 4;
|
||||||
|
float position = 5;
|
||||||
|
bool has_tilt = 6;
|
||||||
|
float tilt = 7;
|
||||||
|
bool stop = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== FAN ====================
|
||||||
|
// ID: 14
|
||||||
message ListEntitiesFanResponse {
|
message ListEntitiesFanResponse {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
@ -131,6 +210,31 @@ message ListEntitiesFanResponse {
|
|||||||
bool supports_oscillation = 5;
|
bool supports_oscillation = 5;
|
||||||
bool supports_speed = 6;
|
bool supports_speed = 6;
|
||||||
}
|
}
|
||||||
|
enum FanSpeed {
|
||||||
|
LOW = 0;
|
||||||
|
MEDIUM = 1;
|
||||||
|
HIGH = 2;
|
||||||
|
}
|
||||||
|
// ID: 23
|
||||||
|
message FanStateResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
bool state = 2;
|
||||||
|
bool oscillating = 3;
|
||||||
|
FanSpeed speed = 4;
|
||||||
|
}
|
||||||
|
// ID: 31
|
||||||
|
message FanCommandRequest {
|
||||||
|
fixed32 key = 1;
|
||||||
|
bool has_state = 2;
|
||||||
|
bool state = 3;
|
||||||
|
bool has_speed = 4;
|
||||||
|
FanSpeed speed = 5;
|
||||||
|
bool has_oscillating = 6;
|
||||||
|
bool oscillating = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== LIGHT ====================
|
||||||
|
// ID: 15
|
||||||
message ListEntitiesLightResponse {
|
message ListEntitiesLightResponse {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
@ -145,69 +249,7 @@ message ListEntitiesLightResponse {
|
|||||||
float max_mireds = 10;
|
float max_mireds = 10;
|
||||||
repeated string effects = 11;
|
repeated string effects = 11;
|
||||||
}
|
}
|
||||||
message ListEntitiesSensorResponse {
|
// ID: 24
|
||||||
string object_id = 1;
|
|
||||||
fixed32 key = 2;
|
|
||||||
string name = 3;
|
|
||||||
string unique_id = 4;
|
|
||||||
|
|
||||||
string icon = 5;
|
|
||||||
string unit_of_measurement = 6;
|
|
||||||
int32 accuracy_decimals = 7;
|
|
||||||
}
|
|
||||||
message ListEntitiesSwitchResponse {
|
|
||||||
string object_id = 1;
|
|
||||||
fixed32 key = 2;
|
|
||||||
string name = 3;
|
|
||||||
string unique_id = 4;
|
|
||||||
|
|
||||||
string icon = 5;
|
|
||||||
bool optimistic = 6;
|
|
||||||
}
|
|
||||||
message ListEntitiesTextSensorResponse {
|
|
||||||
string object_id = 1;
|
|
||||||
fixed32 key = 2;
|
|
||||||
string name = 3;
|
|
||||||
string unique_id = 4;
|
|
||||||
|
|
||||||
string icon = 5;
|
|
||||||
}
|
|
||||||
message ListEntitiesCameraResponse {
|
|
||||||
string object_id = 1;
|
|
||||||
fixed32 key = 2;
|
|
||||||
string name = 3;
|
|
||||||
string unique_id = 4;
|
|
||||||
}
|
|
||||||
message ListEntitiesDoneResponse {
|
|
||||||
// Empty
|
|
||||||
}
|
|
||||||
|
|
||||||
message SubscribeStatesRequest {
|
|
||||||
// Empty
|
|
||||||
}
|
|
||||||
message BinarySensorStateResponse {
|
|
||||||
fixed32 key = 1;
|
|
||||||
bool state = 2;
|
|
||||||
}
|
|
||||||
message CoverStateResponse {
|
|
||||||
fixed32 key = 1;
|
|
||||||
enum CoverState {
|
|
||||||
OPEN = 0;
|
|
||||||
CLOSED = 1;
|
|
||||||
}
|
|
||||||
CoverState state = 2;
|
|
||||||
}
|
|
||||||
enum FanSpeed {
|
|
||||||
LOW = 0;
|
|
||||||
MEDIUM = 1;
|
|
||||||
HIGH = 2;
|
|
||||||
}
|
|
||||||
message FanStateResponse {
|
|
||||||
fixed32 key = 1;
|
|
||||||
bool state = 2;
|
|
||||||
bool oscillating = 3;
|
|
||||||
FanSpeed speed = 4;
|
|
||||||
}
|
|
||||||
message LightStateResponse {
|
message LightStateResponse {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool state = 2;
|
bool state = 2;
|
||||||
@ -219,43 +261,7 @@ message LightStateResponse {
|
|||||||
float color_temperature = 8;
|
float color_temperature = 8;
|
||||||
string effect = 9;
|
string effect = 9;
|
||||||
}
|
}
|
||||||
message SensorStateResponse {
|
// ID: 32
|
||||||
fixed32 key = 1;
|
|
||||||
float state = 2;
|
|
||||||
}
|
|
||||||
message SwitchStateResponse {
|
|
||||||
fixed32 key = 1;
|
|
||||||
bool state = 2;
|
|
||||||
}
|
|
||||||
message TextSensorStateResponse {
|
|
||||||
fixed32 key = 1;
|
|
||||||
string state = 2;
|
|
||||||
}
|
|
||||||
message CameraImageResponse {
|
|
||||||
fixed32 key = 1;
|
|
||||||
bytes data = 2;
|
|
||||||
bool done = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CoverCommandRequest {
|
|
||||||
fixed32 key = 1;
|
|
||||||
enum CoverCommand {
|
|
||||||
OPEN = 0;
|
|
||||||
CLOSE = 1;
|
|
||||||
STOP = 2;
|
|
||||||
}
|
|
||||||
bool has_state = 2;
|
|
||||||
CoverCommand command = 3;
|
|
||||||
}
|
|
||||||
message FanCommandRequest {
|
|
||||||
fixed32 key = 1;
|
|
||||||
bool has_state = 2;
|
|
||||||
bool state = 3;
|
|
||||||
bool has_speed = 4;
|
|
||||||
FanSpeed speed = 5;
|
|
||||||
bool has_oscillating = 6;
|
|
||||||
bool oscillating = 7;
|
|
||||||
}
|
|
||||||
message LightCommandRequest {
|
message LightCommandRequest {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_state = 2;
|
bool has_state = 2;
|
||||||
@ -277,11 +283,64 @@ message LightCommandRequest {
|
|||||||
bool has_effect = 18;
|
bool has_effect = 18;
|
||||||
string effect = 19;
|
string effect = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== SENSOR ====================
|
||||||
|
// ID: 16
|
||||||
|
message ListEntitiesSensorResponse {
|
||||||
|
string object_id = 1;
|
||||||
|
fixed32 key = 2;
|
||||||
|
string name = 3;
|
||||||
|
string unique_id = 4;
|
||||||
|
|
||||||
|
string icon = 5;
|
||||||
|
string unit_of_measurement = 6;
|
||||||
|
int32 accuracy_decimals = 7;
|
||||||
|
}
|
||||||
|
// ID: 25
|
||||||
|
message SensorStateResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
float state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SWITCH ====================
|
||||||
|
// ID: 17
|
||||||
|
message ListEntitiesSwitchResponse {
|
||||||
|
string object_id = 1;
|
||||||
|
fixed32 key = 2;
|
||||||
|
string name = 3;
|
||||||
|
string unique_id = 4;
|
||||||
|
|
||||||
|
string icon = 5;
|
||||||
|
bool assumed_state = 6;
|
||||||
|
}
|
||||||
|
// ID: 26
|
||||||
|
message SwitchStateResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
bool state = 2;
|
||||||
|
}
|
||||||
|
// ID: 33
|
||||||
message SwitchCommandRequest {
|
message SwitchCommandRequest {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool state = 2;
|
bool state = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== TEXT SENSOR ====================
|
||||||
|
// ID: 18
|
||||||
|
message ListEntitiesTextSensorResponse {
|
||||||
|
string object_id = 1;
|
||||||
|
fixed32 key = 2;
|
||||||
|
string name = 3;
|
||||||
|
string unique_id = 4;
|
||||||
|
|
||||||
|
string icon = 5;
|
||||||
|
}
|
||||||
|
// ID: 27
|
||||||
|
message TextSensorStateResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
string state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SUBSCRIBE LOGS ====================
|
||||||
enum LogLevel {
|
enum LogLevel {
|
||||||
NONE = 0;
|
NONE = 0;
|
||||||
ERROR = 1;
|
ERROR = 1;
|
||||||
@ -291,12 +350,12 @@ enum LogLevel {
|
|||||||
VERBOSE = 5;
|
VERBOSE = 5;
|
||||||
VERY_VERBOSE = 6;
|
VERY_VERBOSE = 6;
|
||||||
}
|
}
|
||||||
|
// ID: 28
|
||||||
message SubscribeLogsRequest {
|
message SubscribeLogsRequest {
|
||||||
LogLevel level = 1;
|
LogLevel level = 1;
|
||||||
bool dump_config = 2;
|
bool dump_config = 2;
|
||||||
}
|
}
|
||||||
|
// ID: 29
|
||||||
message SubscribeLogsResponse {
|
message SubscribeLogsResponse {
|
||||||
LogLevel level = 1;
|
LogLevel level = 1;
|
||||||
string tag = 2;
|
string tag = 2;
|
||||||
@ -304,10 +363,13 @@ message SubscribeLogsResponse {
|
|||||||
bool send_failed = 4;
|
bool send_failed = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== HOMEASSISTANT.SERVICE ====================
|
||||||
|
// ID: 34
|
||||||
message SubscribeServiceCallsRequest {
|
message SubscribeServiceCallsRequest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 35
|
||||||
message ServiceCallResponse {
|
message ServiceCallResponse {
|
||||||
string service = 1;
|
string service = 1;
|
||||||
map<string, string> data = 2;
|
map<string, string> data = 2;
|
||||||
@ -315,32 +377,38 @@ message ServiceCallResponse {
|
|||||||
map<string, string> variables = 4;
|
map<string, string> variables = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
||||||
// 1. Client sends SubscribeHomeAssistantStatesRequest
|
// 1. Client sends SubscribeHomeAssistantStatesRequest
|
||||||
// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async)
|
// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async)
|
||||||
// 3. Client sends HomeAssistantStateResponse for state changes.
|
// 3. Client sends HomeAssistantStateResponse for state changes.
|
||||||
|
// ID: 38
|
||||||
message SubscribeHomeAssistantStatesRequest {
|
message SubscribeHomeAssistantStatesRequest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 39
|
||||||
message SubscribeHomeAssistantStateResponse {
|
message SubscribeHomeAssistantStateResponse {
|
||||||
string entity_id = 1;
|
string entity_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 40
|
||||||
message HomeAssistantStateResponse {
|
message HomeAssistantStateResponse {
|
||||||
string entity_id = 1;
|
string entity_id = 1;
|
||||||
string state = 2;
|
string state = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== IMPORT TIME ====================
|
||||||
|
// ID: 36
|
||||||
message GetTimeRequest {
|
message GetTimeRequest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID: 37
|
||||||
message GetTimeResponse {
|
message GetTimeResponse {
|
||||||
fixed32 epoch_seconds = 1;
|
fixed32 epoch_seconds = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== USER-DEFINES SERVICES ====================
|
||||||
|
|
||||||
message ListEntitiesServicesArgument {
|
message ListEntitiesServicesArgument {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
enum Type {
|
enum Type {
|
||||||
@ -351,36 +419,53 @@ message ListEntitiesServicesArgument {
|
|||||||
}
|
}
|
||||||
Type type = 2;
|
Type type = 2;
|
||||||
}
|
}
|
||||||
|
// ID: 41
|
||||||
message ListEntitiesServicesResponse {
|
message ListEntitiesServicesResponse {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
repeated ListEntitiesServicesArgument args = 3;
|
repeated ListEntitiesServicesArgument args = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExecuteServiceArgument {
|
message ExecuteServiceArgument {
|
||||||
bool bool_ = 1;
|
bool bool_ = 1;
|
||||||
int32 int_ = 2;
|
int32 int_ = 2;
|
||||||
float float_ = 3;
|
float float_ = 3;
|
||||||
string string_ = 4;
|
string string_ = 4;
|
||||||
}
|
}
|
||||||
|
// ID: 42
|
||||||
message ExecuteServiceRequest {
|
message ExecuteServiceRequest {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
repeated ExecuteServiceArgument args = 2;
|
repeated ExecuteServiceArgument args = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== CAMERA ====================
|
||||||
|
// ID: 43
|
||||||
|
message ListEntitiesCameraResponse {
|
||||||
|
string object_id = 1;
|
||||||
|
fixed32 key = 2;
|
||||||
|
string name = 3;
|
||||||
|
string unique_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID: 44
|
||||||
|
message CameraImageResponse {
|
||||||
|
fixed32 key = 1;
|
||||||
|
bytes data = 2;
|
||||||
|
bool done = 3;
|
||||||
|
}
|
||||||
|
// ID: 45
|
||||||
message CameraImageRequest {
|
message CameraImageRequest {
|
||||||
bool single = 1;
|
bool single = 1;
|
||||||
bool stream = 2;
|
bool stream = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== CLIMATE ====================
|
||||||
enum ClimateMode {
|
enum ClimateMode {
|
||||||
OFF = 0;
|
OFF = 0;
|
||||||
AUTO = 1;
|
AUTO = 1;
|
||||||
COOL = 2;
|
COOL = 2;
|
||||||
HEAT = 3;
|
HEAT = 3;
|
||||||
}
|
}
|
||||||
|
// ID: 46
|
||||||
message ListEntitiesClimateResponse {
|
message ListEntitiesClimateResponse {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
@ -395,7 +480,7 @@ message ListEntitiesClimateResponse {
|
|||||||
float visual_temperature_step = 10;
|
float visual_temperature_step = 10;
|
||||||
bool supports_away = 11;
|
bool supports_away = 11;
|
||||||
}
|
}
|
||||||
|
// ID: 47
|
||||||
message ClimateStateResponse {
|
message ClimateStateResponse {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
ClimateMode mode = 2;
|
ClimateMode mode = 2;
|
||||||
@ -405,7 +490,7 @@ message ClimateStateResponse {
|
|||||||
float target_temperature_high = 6;
|
float target_temperature_high = 6;
|
||||||
bool away = 7;
|
bool away = 7;
|
||||||
}
|
}
|
||||||
|
// ID: 48
|
||||||
message ClimateCommandRequest {
|
message ClimateCommandRequest {
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_mode = 2;
|
bool has_mode = 2;
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,685 +1,14 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import socket
|
from typing import Any, Callable, Optional, Tuple
|
||||||
import time
|
|
||||||
from typing import Any, Callable, List, Optional, Tuple, Union, cast, Dict
|
|
||||||
|
|
||||||
import attr
|
|
||||||
from google.protobuf import message
|
|
||||||
|
|
||||||
import aioesphomeapi.api_pb2 as pb
|
import aioesphomeapi.api_pb2 as pb
|
||||||
|
from aioesphomeapi.connection import APIConnection, ConnectionParams
|
||||||
|
from aioesphomeapi.core import APIConnectionError
|
||||||
|
from aioesphomeapi.model import *
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class APIConnectionError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
MESSAGE_TYPE_TO_PROTO = {
|
|
||||||
1: pb.HelloRequest,
|
|
||||||
2: pb.HelloResponse,
|
|
||||||
3: pb.ConnectRequest,
|
|
||||||
4: pb.ConnectResponse,
|
|
||||||
5: pb.DisconnectRequest,
|
|
||||||
6: pb.DisconnectResponse,
|
|
||||||
7: pb.PingRequest,
|
|
||||||
8: pb.PingResponse,
|
|
||||||
9: pb.DeviceInfoRequest,
|
|
||||||
10: pb.DeviceInfoResponse,
|
|
||||||
11: pb.ListEntitiesRequest,
|
|
||||||
12: pb.ListEntitiesBinarySensorResponse,
|
|
||||||
13: pb.ListEntitiesCoverResponse,
|
|
||||||
14: pb.ListEntitiesFanResponse,
|
|
||||||
15: pb.ListEntitiesLightResponse,
|
|
||||||
16: pb.ListEntitiesSensorResponse,
|
|
||||||
17: pb.ListEntitiesSwitchResponse,
|
|
||||||
18: pb.ListEntitiesTextSensorResponse,
|
|
||||||
19: pb.ListEntitiesDoneResponse,
|
|
||||||
20: pb.SubscribeStatesRequest,
|
|
||||||
21: pb.BinarySensorStateResponse,
|
|
||||||
22: pb.CoverStateResponse,
|
|
||||||
23: pb.FanStateResponse,
|
|
||||||
24: pb.LightStateResponse,
|
|
||||||
25: pb.SensorStateResponse,
|
|
||||||
26: pb.SwitchStateResponse,
|
|
||||||
27: pb.TextSensorStateResponse,
|
|
||||||
28: pb.SubscribeLogsRequest,
|
|
||||||
29: pb.SubscribeLogsResponse,
|
|
||||||
30: pb.CoverCommandRequest,
|
|
||||||
31: pb.FanCommandRequest,
|
|
||||||
32: pb.LightCommandRequest,
|
|
||||||
33: pb.SwitchCommandRequest,
|
|
||||||
34: pb.SubscribeServiceCallsRequest,
|
|
||||||
35: pb.ServiceCallResponse,
|
|
||||||
36: pb.GetTimeRequest,
|
|
||||||
37: pb.GetTimeResponse,
|
|
||||||
38: pb.SubscribeHomeAssistantStatesRequest,
|
|
||||||
39: pb.SubscribeHomeAssistantStateResponse,
|
|
||||||
40: pb.HomeAssistantStateResponse,
|
|
||||||
41: pb.ListEntitiesServicesResponse,
|
|
||||||
42: pb.ExecuteServiceRequest,
|
|
||||||
43: pb.ListEntitiesCameraResponse,
|
|
||||||
44: pb.CameraImageResponse,
|
|
||||||
45: pb.CameraImageRequest,
|
|
||||||
46: pb.ListEntitiesClimateResponse,
|
|
||||||
47: pb.ClimateStateResponse,
|
|
||||||
48: pb.ClimateCommandRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _varuint_to_bytes(value: int) -> bytes:
|
|
||||||
if value <= 0x7F:
|
|
||||||
return bytes([value])
|
|
||||||
|
|
||||||
ret = bytes()
|
|
||||||
while value:
|
|
||||||
temp = value & 0x7F
|
|
||||||
value >>= 7
|
|
||||||
if value:
|
|
||||||
ret += bytes([temp | 0x80])
|
|
||||||
else:
|
|
||||||
ret += bytes([temp])
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def _bytes_to_varuint(value: bytes) -> Optional[int]:
|
|
||||||
result = 0
|
|
||||||
bitpos = 0
|
|
||||||
for val in value:
|
|
||||||
result |= (val & 0x7F) << bitpos
|
|
||||||
bitpos += 7
|
|
||||||
if (val & 0x80) == 0:
|
|
||||||
return result
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def resolve_ip_address_getaddrinfo(eventloop: asyncio.events.AbstractEventLoop,
|
|
||||||
host: str, port: int) -> Tuple[Any, ...]:
|
|
||||||
try:
|
|
||||||
res = await eventloop.getaddrinfo(host, port, family=socket.AF_INET,
|
|
||||||
proto=socket.IPPROTO_TCP)
|
|
||||||
except OSError as err:
|
|
||||||
raise APIConnectionError("Error resolving IP address: {}".format(err))
|
|
||||||
|
|
||||||
if not res:
|
|
||||||
raise APIConnectionError("Error resolving IP address: No matches!")
|
|
||||||
|
|
||||||
_, _, _, _, sockaddr = res[0]
|
|
||||||
|
|
||||||
return sockaddr
|
|
||||||
|
|
||||||
|
|
||||||
async def resolve_ip_address(eventloop: asyncio.events.AbstractEventLoop,
|
|
||||||
host: str, port: int) -> Tuple[Any, ...]:
|
|
||||||
try:
|
|
||||||
return await resolve_ip_address_getaddrinfo(eventloop, host, port)
|
|
||||||
except APIConnectionError as err:
|
|
||||||
if host.endswith('.local'):
|
|
||||||
from aioesphomeapi.host_resolver import resolve_host
|
|
||||||
|
|
||||||
return await eventloop.run_in_executor(None, resolve_host, host), port
|
|
||||||
raise err
|
|
||||||
|
|
||||||
|
|
||||||
# Wrap some types in attr classes to make them serializable
|
|
||||||
@attr.s
|
|
||||||
class DeviceInfo:
|
|
||||||
uses_password = attr.ib(type=bool)
|
|
||||||
name = attr.ib(type=str)
|
|
||||||
mac_address = attr.ib(type=str)
|
|
||||||
esphome_core_version = attr.ib(type=str)
|
|
||||||
compilation_time = attr.ib(type=str)
|
|
||||||
model = attr.ib(type=str)
|
|
||||||
has_deep_sleep = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class EntityInfo:
|
|
||||||
object_id = attr.ib(type=str)
|
|
||||||
key = attr.ib(type=int)
|
|
||||||
name = attr.ib(type=str)
|
|
||||||
unique_id = attr.ib(type=str)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class EntityState:
|
|
||||||
key = attr.ib(type=int)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class BinarySensorInfo(EntityInfo):
|
|
||||||
device_class = attr.ib(type=str)
|
|
||||||
is_status_binary_sensor = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class BinarySensorState(EntityState):
|
|
||||||
state = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class CoverInfo(EntityInfo):
|
|
||||||
is_optimistic = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
COVER_STATE_OPEN = 0
|
|
||||||
COVER_SATE_CLOSED = 1
|
|
||||||
COVER_STATES = [COVER_STATE_OPEN, COVER_SATE_CLOSED]
|
|
||||||
|
|
||||||
COVER_COMMAND_OPEN = 0
|
|
||||||
COVER_COMMAND_CLOSE = 1
|
|
||||||
COVER_COMMAND_STOP = 2
|
|
||||||
COVER_COMMANDS = [COVER_COMMAND_OPEN, COVER_COMMAND_CLOSE, COVER_COMMAND_STOP]
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class CoverState(EntityState):
|
|
||||||
state = attr.ib(type=int, converter=int,
|
|
||||||
validator=attr.validators.in_(COVER_STATES))
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class FanInfo(EntityInfo):
|
|
||||||
supports_oscillation = attr.ib(type=bool)
|
|
||||||
supports_speed = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
FAN_SPEED_LOW = 0
|
|
||||||
FAN_SPEED_MEDIUM = 1
|
|
||||||
FAN_SPEED_HIGH = 2
|
|
||||||
FAN_SPEEDS = [FAN_SPEED_LOW, FAN_SPEED_MEDIUM, FAN_SPEED_HIGH]
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class FanState(EntityState):
|
|
||||||
state = attr.ib(type=bool)
|
|
||||||
oscillating = attr.ib(type=bool)
|
|
||||||
speed = attr.ib(type=int, converter=int,
|
|
||||||
validator=attr.validators.in_(FAN_SPEEDS))
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class LightInfo(EntityInfo):
|
|
||||||
supports_brightness = attr.ib(type=bool)
|
|
||||||
supports_rgb = attr.ib(type=bool)
|
|
||||||
supports_white_value = attr.ib(type=bool)
|
|
||||||
supports_color_temperature = attr.ib(type=bool)
|
|
||||||
min_mireds = attr.ib(type=float)
|
|
||||||
max_mireds = attr.ib(type=float)
|
|
||||||
effects = attr.ib(type=List[str], converter=list)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class LightState(EntityState):
|
|
||||||
state = attr.ib(type=bool)
|
|
||||||
brightness = attr.ib(type=float)
|
|
||||||
red = attr.ib(type=float)
|
|
||||||
green = attr.ib(type=float)
|
|
||||||
blue = attr.ib(type=float)
|
|
||||||
white = attr.ib(type=float)
|
|
||||||
color_temperature = attr.ib(type=float)
|
|
||||||
effect = attr.ib(type=str)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class SensorInfo(EntityInfo):
|
|
||||||
icon = attr.ib(type=str)
|
|
||||||
unit_of_measurement = attr.ib(type=str)
|
|
||||||
accuracy_decimals = attr.ib(type=int)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class SensorState(EntityState):
|
|
||||||
state = attr.ib(type=float)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class SwitchInfo(EntityInfo):
|
|
||||||
icon = attr.ib(type=str)
|
|
||||||
optimistic = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class SwitchState(EntityState):
|
|
||||||
state = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class TextSensorInfo(EntityInfo):
|
|
||||||
icon = attr.ib(type=str)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class TextSensorState(EntityState):
|
|
||||||
state = attr.ib(type=str)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class CameraInfo(EntityInfo):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class CameraState(EntityState):
|
|
||||||
image = attr.ib(type=bytes)
|
|
||||||
|
|
||||||
|
|
||||||
CLIMATE_MODE_OFF = 0
|
|
||||||
CLIMATE_MODE_AUTO = 1
|
|
||||||
CLIMATE_MODE_COOL = 2
|
|
||||||
CLIMATE_MODE_HEAT = 3
|
|
||||||
CLIMATE_MODES = [CLIMATE_MODE_OFF, CLIMATE_MODE_AUTO, CLIMATE_MODE_COOL, CLIMATE_MODE_HEAT]
|
|
||||||
_validate_climate_mode = attr.validators.in_(CLIMATE_MODES)
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_climate_modes(value):
|
|
||||||
return [int(val) for val in value]
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class ClimateInfo(EntityInfo):
|
|
||||||
supports_current_temperature = attr.ib(type=bool)
|
|
||||||
supports_two_point_target_temperature = attr.ib(type=bool)
|
|
||||||
supported_modes = attr.ib(type=List[int], converter=_convert_climate_modes)
|
|
||||||
visual_min_temperature = attr.ib(type=float)
|
|
||||||
visual_max_temperature = attr.ib(type=float)
|
|
||||||
visual_temperature_step = attr.ib(type=float)
|
|
||||||
supports_away = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class ClimateState(EntityState):
|
|
||||||
mode = attr.ib(type=int, converter=int, validator=_validate_climate_mode)
|
|
||||||
current_temperature = attr.ib(type=float)
|
|
||||||
target_temperature = attr.ib(type=float)
|
|
||||||
target_temperature_low = attr.ib(type=float)
|
|
||||||
target_temperature_high = attr.ib(type=float)
|
|
||||||
away = attr.ib(type=bool)
|
|
||||||
|
|
||||||
|
|
||||||
COMPONENT_TYPE_TO_INFO = {
|
|
||||||
'binary_sensor': BinarySensorInfo,
|
|
||||||
'cover': CoverInfo,
|
|
||||||
'fan': FanInfo,
|
|
||||||
'light': LightInfo,
|
|
||||||
'sensor': SensorInfo,
|
|
||||||
'switch': SwitchInfo,
|
|
||||||
'text_sensor': TextSensorInfo,
|
|
||||||
'camera': CameraInfo,
|
|
||||||
'climate': ClimateInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class ServiceCall:
|
|
||||||
service = attr.ib(type=str)
|
|
||||||
data = attr.ib(type=Dict[str, str], converter=dict)
|
|
||||||
data_template = attr.ib(type=Dict[str, str], converter=dict)
|
|
||||||
variables = attr.ib(type=Dict[str, str], converter=dict)
|
|
||||||
|
|
||||||
|
|
||||||
USER_SERVICE_ARG_BOOL = 0
|
|
||||||
USER_SERVICE_ARG_INT = 1
|
|
||||||
USER_SERVICE_ARG_FLOAT = 2
|
|
||||||
USER_SERVICE_ARG_STRING = 3
|
|
||||||
USER_SERVICE_ARG_TYPES = [
|
|
||||||
USER_SERVICE_ARG_BOOL, USER_SERVICE_ARG_INT, USER_SERVICE_ARG_FLOAT, USER_SERVICE_ARG_STRING
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _attr_obj_from_dict(cls, **kwargs):
|
|
||||||
return cls(**{key: kwargs[key] for key in attr.fields_dict(cls)})
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class UserServiceArg:
|
|
||||||
name = attr.ib(type=str)
|
|
||||||
type_ = attr.ib(type=int, converter=int,
|
|
||||||
validator=attr.validators.in_(USER_SERVICE_ARG_TYPES))
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class UserService:
|
|
||||||
name = attr.ib(type=str)
|
|
||||||
key = attr.ib(type=int)
|
|
||||||
args = attr.ib(type=List[UserServiceArg], converter=list)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(dict_):
|
|
||||||
args = []
|
|
||||||
for arg in dict_.get('args', []):
|
|
||||||
args.append(_attr_obj_from_dict(UserServiceArg, **arg))
|
|
||||||
return UserService(
|
|
||||||
name=dict_.get('name', ''),
|
|
||||||
key=dict_.get('key', 0),
|
|
||||||
args=args
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'name': self.name,
|
|
||||||
'key': self.key,
|
|
||||||
'args': [attr.asdict(arg) for arg in self.args],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class ConnectionParams:
|
|
||||||
eventloop = attr.ib(type=asyncio.events.AbstractEventLoop)
|
|
||||||
address = attr.ib(type=str)
|
|
||||||
port = attr.ib(type=int)
|
|
||||||
password = attr.ib(type=Optional[str])
|
|
||||||
client_info = attr.ib(type=str)
|
|
||||||
keepalive = attr.ib(type=float)
|
|
||||||
|
|
||||||
|
|
||||||
class APIConnection:
|
|
||||||
def __init__(self, params: ConnectionParams, on_stop):
|
|
||||||
self._params = params
|
|
||||||
self.on_stop = on_stop
|
|
||||||
self._stopped = False
|
|
||||||
self._socket = None # type: Optional[socket.socket]
|
|
||||||
self._socket_reader = None # type: Optional[asyncio.StreamReader]
|
|
||||||
self._socket_writer = None # type: Optional[asyncio.StreamWriter]
|
|
||||||
self._write_lock = asyncio.Lock()
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
self._socket_connected = False
|
|
||||||
self._state_lock = asyncio.Lock()
|
|
||||||
|
|
||||||
self._message_handlers = [] # type: List[Callable[[message], None]]
|
|
||||||
|
|
||||||
self._running_task = None # type: Optional[asyncio.Task]
|
|
||||||
|
|
||||||
def _start_ping(self) -> None:
|
|
||||||
async def func() -> None:
|
|
||||||
while self._connected:
|
|
||||||
await asyncio.sleep(self._params.keepalive)
|
|
||||||
|
|
||||||
if not self._connected:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ping()
|
|
||||||
except APIConnectionError:
|
|
||||||
_LOGGER.info("%s: Ping Failed!", self._params.address)
|
|
||||||
await self._on_error()
|
|
||||||
return
|
|
||||||
|
|
||||||
self._params.eventloop.create_task(func())
|
|
||||||
|
|
||||||
async def _close_socket(self) -> None:
|
|
||||||
if not self._socket_connected:
|
|
||||||
return
|
|
||||||
async with self._write_lock:
|
|
||||||
self._socket_writer.close()
|
|
||||||
self._socket_writer = None
|
|
||||||
self._socket_reader = None
|
|
||||||
if self._socket is not None:
|
|
||||||
self._socket.close()
|
|
||||||
self._socket_connected = False
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
_LOGGER.debug("%s: Closed socket", self._params.address)
|
|
||||||
|
|
||||||
async def stop(self, force: bool = False) -> None:
|
|
||||||
if self._stopped:
|
|
||||||
return
|
|
||||||
if self._connected and not force:
|
|
||||||
try:
|
|
||||||
await self._disconnect()
|
|
||||||
except APIConnectionError:
|
|
||||||
pass
|
|
||||||
self._stopped = True
|
|
||||||
if self._running_task is not None:
|
|
||||||
self._running_task.cancel()
|
|
||||||
await self._close_socket()
|
|
||||||
await self.on_stop()
|
|
||||||
|
|
||||||
async def _on_error(self) -> None:
|
|
||||||
await self.stop(force=True)
|
|
||||||
|
|
||||||
async def connect(self) -> None:
|
|
||||||
if self._stopped:
|
|
||||||
raise APIConnectionError("Connection is closed!")
|
|
||||||
if self._connected:
|
|
||||||
raise APIConnectionError("Already connected!")
|
|
||||||
|
|
||||||
try:
|
|
||||||
coro = resolve_ip_address(self._params.eventloop, self._params.address,
|
|
||||||
self._params.port)
|
|
||||||
sockaddr = await asyncio.wait_for(coro, 30.0)
|
|
||||||
except APIConnectionError as err:
|
|
||||||
await self._on_error()
|
|
||||||
raise err
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await self._on_error()
|
|
||||||
raise APIConnectionError("Timeout while resolving IP address")
|
|
||||||
|
|
||||||
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self._socket.setblocking(False)
|
|
||||||
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
||||||
|
|
||||||
_LOGGER.debug("%s: Connecting to %s:%s (%s)", self._params.address,
|
|
||||||
self._params.address, self._params.port, sockaddr)
|
|
||||||
try:
|
|
||||||
coro = self._params.eventloop.sock_connect(self._socket, sockaddr)
|
|
||||||
await asyncio.wait_for(coro, 30.0)
|
|
||||||
except OSError as err:
|
|
||||||
await self._on_error()
|
|
||||||
raise APIConnectionError("Error connecting to {}: {}".format(sockaddr, err))
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await self._on_error()
|
|
||||||
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)
|
|
||||||
self._socket_connected = True
|
|
||||||
self._params.eventloop.create_task(self.run_forever())
|
|
||||||
|
|
||||||
hello = pb.HelloRequest()
|
|
||||||
hello.client_info = self._params.client_info
|
|
||||||
try:
|
|
||||||
resp = await self.send_message_await_response(hello, pb.HelloResponse)
|
|
||||||
except APIConnectionError as err:
|
|
||||||
await self._on_error()
|
|
||||||
raise err
|
|
||||||
_LOGGER.debug("%s: Successfully connected to %s ('%s' API=%s.%s)",
|
|
||||||
self._params.address, self._params.address,
|
|
||||||
resp.server_info, resp.api_version_major, resp.api_version_minor)
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
self._start_ping()
|
|
||||||
|
|
||||||
async def login(self) -> None:
|
|
||||||
self._check_connected()
|
|
||||||
if self._authenticated:
|
|
||||||
raise APIConnectionError("Already logged in!")
|
|
||||||
|
|
||||||
connect = pb.ConnectRequest()
|
|
||||||
if self._params.password is not None:
|
|
||||||
connect.password = self._params.password
|
|
||||||
resp = await self.send_message_await_response(connect, pb.ConnectResponse)
|
|
||||||
if resp.invalid_password:
|
|
||||||
raise APIConnectionError("Invalid password!")
|
|
||||||
|
|
||||||
self._authenticated = True
|
|
||||||
|
|
||||||
def _check_connected(self) -> None:
|
|
||||||
if not self._connected:
|
|
||||||
raise APIConnectionError("Must be connected!")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
return self._connected
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_authenticated(self) -> bool:
|
|
||||||
return self._authenticated
|
|
||||||
|
|
||||||
async def _write(self, data: bytes) -> None:
|
|
||||||
_LOGGER.debug("%s: Write: %s", self._params.address,
|
|
||||||
' '.join('{:02X}'.format(x) for x in data))
|
|
||||||
if not self._socket_connected:
|
|
||||||
raise APIConnectionError("Socket is not connected")
|
|
||||||
try:
|
|
||||||
async with self._write_lock:
|
|
||||||
self._socket_writer.write(data)
|
|
||||||
await self._socket_writer.drain()
|
|
||||||
except OSError as err:
|
|
||||||
await self._on_error()
|
|
||||||
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():
|
|
||||||
if isinstance(msg, klass):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
encoded = msg.SerializeToString()
|
|
||||||
_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)
|
|
||||||
req += encoded
|
|
||||||
await self._write(req)
|
|
||||||
|
|
||||||
async def send_message_callback_response(self, send_msg: message.Message,
|
|
||||||
on_message: Callable[[Any], None]) -> None:
|
|
||||||
self._message_handlers.append(on_message)
|
|
||||||
await self.send_message(send_msg)
|
|
||||||
|
|
||||||
async def send_message_await_response_complex(self, send_msg: message.Message,
|
|
||||||
do_append: Callable[[Any], bool],
|
|
||||||
do_stop: Callable[[Any], bool],
|
|
||||||
timeout: float = 5.0) -> List[Any]:
|
|
||||||
fut = self._params.eventloop.create_future()
|
|
||||||
responses = []
|
|
||||||
|
|
||||||
def on_message(resp):
|
|
||||||
if fut.done():
|
|
||||||
return
|
|
||||||
if do_append(resp):
|
|
||||||
responses.append(resp)
|
|
||||||
if do_stop(resp):
|
|
||||||
fut.set_result(responses)
|
|
||||||
|
|
||||||
self._message_handlers.append(on_message)
|
|
||||||
await self.send_message(send_msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(fut, timeout)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
if self._stopped:
|
|
||||||
raise APIConnectionError("Disconnected while waiting for API response!")
|
|
||||||
raise APIConnectionError("Timeout while waiting for API response!")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._message_handlers.remove(on_message)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return responses
|
|
||||||
|
|
||||||
async def send_message_await_response(self,
|
|
||||||
send_msg: message.Message,
|
|
||||||
response_type: Any, timeout: float = 5.0) -> Any:
|
|
||||||
def is_response(msg):
|
|
||||||
return isinstance(msg, response_type)
|
|
||||||
|
|
||||||
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)))
|
|
||||||
|
|
||||||
return res[0]
|
|
||||||
|
|
||||||
async def _recv(self, amount: int) -> bytes:
|
|
||||||
if amount == 0:
|
|
||||||
return bytes()
|
|
||||||
|
|
||||||
try:
|
|
||||||
ret = await self._socket_reader.readexactly(amount)
|
|
||||||
except (asyncio.IncompleteReadError, OSError, TimeoutError) as err:
|
|
||||||
raise APIConnectionError("Error while receiving data: {}".format(err))
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
async def _recv_varint(self) -> int:
|
|
||||||
raw = bytes()
|
|
||||||
while not raw or raw[-1] & 0x80:
|
|
||||||
raw += await self._recv(1)
|
|
||||||
return cast(int, _bytes_to_varuint(raw))
|
|
||||||
|
|
||||||
async def _run_once(self) -> None:
|
|
||||||
preamble = await self._recv(1)
|
|
||||||
if preamble[0] != 0x00:
|
|
||||||
raise APIConnectionError("Invalid preamble")
|
|
||||||
|
|
||||||
length = await self._recv_varint()
|
|
||||||
msg_type = await self._recv_varint()
|
|
||||||
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
for msg_handler in self._message_handlers[:]:
|
|
||||||
msg_handler(msg)
|
|
||||||
await self._handle_internal_messages(msg)
|
|
||||||
|
|
||||||
async def run_forever(self) -> None:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
await self._run_once()
|
|
||||||
except APIConnectionError as err:
|
|
||||||
_LOGGER.info("%s: Error while reading incoming messages: %s",
|
|
||||||
self._params.address, err)
|
|
||||||
await self._on_error()
|
|
||||||
break
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.info("%s: Unexpected error while reading incoming messages: %s",
|
|
||||||
self._params.address, err)
|
|
||||||
await self._on_error()
|
|
||||||
break
|
|
||||||
|
|
||||||
async def _handle_internal_messages(self, msg: Any) -> None:
|
|
||||||
if isinstance(msg, pb.DisconnectRequest):
|
|
||||||
await self.send_message(pb.DisconnectResponse())
|
|
||||||
await self.stop(force=True)
|
|
||||||
elif isinstance(msg, pb.PingRequest):
|
|
||||||
await self.send_message(pb.PingResponse())
|
|
||||||
elif isinstance(msg, pb.GetTimeRequest):
|
|
||||||
resp = pb.GetTimeResponse()
|
|
||||||
resp.epoch_seconds = int(time.time())
|
|
||||||
await self.send_message(resp)
|
|
||||||
|
|
||||||
async def ping(self) -> None:
|
|
||||||
self._check_connected()
|
|
||||||
await self.send_message_await_response(pb.PingRequest(), pb.PingResponse)
|
|
||||||
|
|
||||||
async def _disconnect(self) -> None:
|
|
||||||
self._check_connected()
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.send_message_await_response(pb.DisconnectRequest(), pb.DisconnectResponse)
|
|
||||||
except APIConnectionError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _check_authenticated(self) -> None:
|
|
||||||
if not self._authenticated:
|
|
||||||
raise APIConnectionError("Must login first!")
|
|
||||||
|
|
||||||
|
|
||||||
class APIClient:
|
class APIClient:
|
||||||
def __init__(self, eventloop, address: str, port: int, password: str, *,
|
def __init__(self, eventloop, address: str, port: int, password: str, *,
|
||||||
client_info: str = 'aioesphomeapi', keepalive: float = 15.0):
|
client_info: str = 'aioesphomeapi', keepalive: float = 15.0):
|
||||||
@ -888,22 +217,37 @@ class APIClient:
|
|||||||
|
|
||||||
async def cover_command(self,
|
async def cover_command(self,
|
||||||
key: int,
|
key: int,
|
||||||
command: int
|
position: Optional[float] = None,
|
||||||
|
tilt: Optional[float] = None,
|
||||||
|
stop: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._check_authenticated()
|
self._check_authenticated()
|
||||||
|
|
||||||
req = pb.CoverCommandRequest()
|
req = pb.CoverCommandRequest()
|
||||||
req.key = key
|
req.key = key
|
||||||
req.has_state = True
|
if self.api_version >= APIVersion(1, 1):
|
||||||
if command not in COVER_COMMANDS:
|
if position is not None:
|
||||||
raise ValueError
|
req.has_position = True
|
||||||
req.command = command
|
req.position = position
|
||||||
|
if tilt is not None:
|
||||||
|
req.has_tilt = True
|
||||||
|
req.tilt = tilt
|
||||||
|
if stop:
|
||||||
|
req.stop = stop
|
||||||
|
else:
|
||||||
|
req.has_legacy_command = True
|
||||||
|
if stop:
|
||||||
|
req.legacy_command = LegacyCoverCommand.STOP
|
||||||
|
elif position == 1.0:
|
||||||
|
req.legacy_command = LegacyCoverCommand.OPEN
|
||||||
|
else:
|
||||||
|
req.legacy_command = LegacyCoverCommand.CLOSE
|
||||||
await self._connection.send_message(req)
|
await self._connection.send_message(req)
|
||||||
|
|
||||||
async def fan_command(self,
|
async def fan_command(self,
|
||||||
key: int,
|
key: int,
|
||||||
state: Optional[bool] = None,
|
state: Optional[bool] = None,
|
||||||
speed: Optional[int] = None,
|
speed: Optional[FanSpeed] = None,
|
||||||
oscillating: Optional[bool] = None
|
oscillating: Optional[bool] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self._check_authenticated()
|
self._check_authenticated()
|
||||||
@ -915,8 +259,6 @@ class APIClient:
|
|||||||
req.state = state
|
req.state = state
|
||||||
if speed is not None:
|
if speed is not None:
|
||||||
req.has_speed = True
|
req.has_speed = True
|
||||||
if speed not in FAN_SPEEDS:
|
|
||||||
raise ValueError
|
|
||||||
req.speed = speed
|
req.speed = speed
|
||||||
if oscillating is not None:
|
if oscillating is not None:
|
||||||
req.has_oscillating = True
|
req.has_oscillating = True
|
||||||
@ -979,7 +321,7 @@ class APIClient:
|
|||||||
|
|
||||||
async def climate_command(self,
|
async def climate_command(self,
|
||||||
key: int,
|
key: int,
|
||||||
mode: Optional[int] = None,
|
mode: Optional[ClimateMode] = None,
|
||||||
target_temperature: Optional[float] = None,
|
target_temperature: Optional[float] = None,
|
||||||
target_temperature_low: Optional[float] = None,
|
target_temperature_low: Optional[float] = None,
|
||||||
target_temperature_high: Optional[float] = None,
|
target_temperature_high: Optional[float] = None,
|
||||||
@ -1016,10 +358,10 @@ class APIClient:
|
|||||||
arg = pb.ExecuteServiceArgument()
|
arg = pb.ExecuteServiceArgument()
|
||||||
val = data[arg_desc.name]
|
val = data[arg_desc.name]
|
||||||
attr_ = {
|
attr_ = {
|
||||||
USER_SERVICE_ARG_BOOL: 'bool_',
|
UserServiceArgType.BOOL: 'bool_',
|
||||||
USER_SERVICE_ARG_INT: 'int_',
|
UserServiceArgType.INT: 'int_',
|
||||||
USER_SERVICE_ARG_FLOAT: 'float_',
|
UserServiceArgType.FLOAT: 'float_',
|
||||||
USER_SERVICE_ARG_STRING: 'string_',
|
UserServiceArgType.STRING: 'string_',
|
||||||
}[arg_desc.type_]
|
}[arg_desc.type_]
|
||||||
setattr(arg, attr_, val)
|
setattr(arg, attr_, val)
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
@ -1037,3 +379,9 @@ class APIClient:
|
|||||||
|
|
||||||
async def request_image_stream(self):
|
async def request_image_stream(self):
|
||||||
await self._request_image(stream=True)
|
await self._request_image(stream=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_version(self) -> Optional[APIVersion]:
|
||||||
|
if self._connection is None:
|
||||||
|
return None
|
||||||
|
return self._connection.api_version
|
||||||
|
341
aioesphomeapi/connection.py
Normal file
341
aioesphomeapi/connection.py
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, List, Optional, cast
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from google.protobuf import message
|
||||||
|
|
||||||
|
import aioesphomeapi.api_pb2 as pb
|
||||||
|
from aioesphomeapi.core import APIConnectionError, MESSAGE_TYPE_TO_PROTO
|
||||||
|
from aioesphomeapi.model import APIVersion
|
||||||
|
from aioesphomeapi.util import _bytes_to_varuint, _varuint_to_bytes, resolve_ip_address
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class ConnectionParams:
|
||||||
|
eventloop = attr.ib(type=asyncio.events.AbstractEventLoop)
|
||||||
|
address = attr.ib(type=str)
|
||||||
|
port = attr.ib(type=int)
|
||||||
|
password = attr.ib(type=Optional[str])
|
||||||
|
client_info = attr.ib(type=str)
|
||||||
|
keepalive = attr.ib(type=float)
|
||||||
|
|
||||||
|
|
||||||
|
class APIConnection:
|
||||||
|
def __init__(self, params: ConnectionParams, on_stop):
|
||||||
|
self._params = params
|
||||||
|
self.on_stop = on_stop
|
||||||
|
self._stopped = False
|
||||||
|
self._socket = None # type: Optional[socket.socket]
|
||||||
|
self._socket_reader = None # type: Optional[asyncio.StreamReader]
|
||||||
|
self._socket_writer = None # type: Optional[asyncio.StreamWriter]
|
||||||
|
self._write_lock = asyncio.Lock()
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
self._socket_connected = False
|
||||||
|
self._state_lock = asyncio.Lock()
|
||||||
|
self._api_version = None # type: Optional[APIVersion]
|
||||||
|
|
||||||
|
self._message_handlers = [] # type: List[Callable[[message], None]]
|
||||||
|
|
||||||
|
self._running_task = None # type: Optional[asyncio.Task]
|
||||||
|
|
||||||
|
def _start_ping(self) -> None:
|
||||||
|
async def func() -> None:
|
||||||
|
while self._connected:
|
||||||
|
await asyncio.sleep(self._params.keepalive)
|
||||||
|
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.ping()
|
||||||
|
except APIConnectionError:
|
||||||
|
_LOGGER.info("%s: Ping Failed!", self._params.address)
|
||||||
|
await self._on_error()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._params.eventloop.create_task(func())
|
||||||
|
|
||||||
|
async def _close_socket(self) -> None:
|
||||||
|
if not self._socket_connected:
|
||||||
|
return
|
||||||
|
async with self._write_lock:
|
||||||
|
self._socket_writer.close()
|
||||||
|
self._socket_writer = None
|
||||||
|
self._socket_reader = None
|
||||||
|
if self._socket is not None:
|
||||||
|
self._socket.close()
|
||||||
|
self._socket_connected = False
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
_LOGGER.debug("%s: Closed socket", self._params.address)
|
||||||
|
|
||||||
|
async def stop(self, force: bool = False) -> None:
|
||||||
|
if self._stopped:
|
||||||
|
return
|
||||||
|
if self._connected and not force:
|
||||||
|
try:
|
||||||
|
await self._disconnect()
|
||||||
|
except APIConnectionError:
|
||||||
|
pass
|
||||||
|
self._stopped = True
|
||||||
|
if self._running_task is not None:
|
||||||
|
self._running_task.cancel()
|
||||||
|
await self._close_socket()
|
||||||
|
await self.on_stop()
|
||||||
|
|
||||||
|
async def _on_error(self) -> None:
|
||||||
|
await self.stop(force=True)
|
||||||
|
|
||||||
|
async def connect(self) -> None:
|
||||||
|
if self._stopped:
|
||||||
|
raise APIConnectionError("Connection is closed!")
|
||||||
|
if self._connected:
|
||||||
|
raise APIConnectionError("Already connected!")
|
||||||
|
|
||||||
|
try:
|
||||||
|
coro = resolve_ip_address(self._params.eventloop, self._params.address,
|
||||||
|
self._params.port)
|
||||||
|
sockaddr = await asyncio.wait_for(coro, 30.0)
|
||||||
|
except APIConnectionError as err:
|
||||||
|
await self._on_error()
|
||||||
|
raise err
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await self._on_error()
|
||||||
|
raise APIConnectionError("Timeout while resolving IP address")
|
||||||
|
|
||||||
|
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self._socket.setblocking(False)
|
||||||
|
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
|
|
||||||
|
_LOGGER.debug("%s: Connecting to %s:%s (%s)", self._params.address,
|
||||||
|
self._params.address, self._params.port, sockaddr)
|
||||||
|
try:
|
||||||
|
coro = self._params.eventloop.sock_connect(self._socket, sockaddr)
|
||||||
|
await asyncio.wait_for(coro, 30.0)
|
||||||
|
except OSError as err:
|
||||||
|
await self._on_error()
|
||||||
|
raise APIConnectionError("Error connecting to {}: {}".format(sockaddr, err))
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await self._on_error()
|
||||||
|
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)
|
||||||
|
self._socket_connected = True
|
||||||
|
self._params.eventloop.create_task(self.run_forever())
|
||||||
|
|
||||||
|
hello = pb.HelloRequest()
|
||||||
|
hello.client_info = self._params.client_info
|
||||||
|
try:
|
||||||
|
resp = await self.send_message_await_response(hello, pb.HelloResponse)
|
||||||
|
except APIConnectionError as err:
|
||||||
|
await self._on_error()
|
||||||
|
raise err
|
||||||
|
_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)
|
||||||
|
if self._api_version.major > 2:
|
||||||
|
_LOGGER.error("%s: Incompatible version %s! Closing connection",
|
||||||
|
self._api_version.major)
|
||||||
|
await self._on_error()
|
||||||
|
raise APIConnectionError("Incompatible API version.")
|
||||||
|
self._connected = True
|
||||||
|
|
||||||
|
self._start_ping()
|
||||||
|
|
||||||
|
async def login(self) -> None:
|
||||||
|
self._check_connected()
|
||||||
|
if self._authenticated:
|
||||||
|
raise APIConnectionError("Already logged in!")
|
||||||
|
|
||||||
|
connect = pb.ConnectRequest()
|
||||||
|
if self._params.password is not None:
|
||||||
|
connect.password = self._params.password
|
||||||
|
resp = await self.send_message_await_response(connect, pb.ConnectResponse)
|
||||||
|
if resp.invalid_password:
|
||||||
|
raise APIConnectionError("Invalid password!")
|
||||||
|
|
||||||
|
self._authenticated = True
|
||||||
|
|
||||||
|
def _check_connected(self) -> None:
|
||||||
|
if not self._connected:
|
||||||
|
raise APIConnectionError("Must be connected!")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
return self._connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self) -> bool:
|
||||||
|
return self._authenticated
|
||||||
|
|
||||||
|
async def _write(self, data: bytes) -> None:
|
||||||
|
_LOGGER.debug("%s: Write: %s", self._params.address,
|
||||||
|
' '.join('{:02X}'.format(x) for x in data))
|
||||||
|
if not self._socket_connected:
|
||||||
|
raise APIConnectionError("Socket is not connected")
|
||||||
|
try:
|
||||||
|
async with self._write_lock:
|
||||||
|
self._socket_writer.write(data)
|
||||||
|
await self._socket_writer.drain()
|
||||||
|
except OSError as err:
|
||||||
|
await self._on_error()
|
||||||
|
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():
|
||||||
|
if isinstance(msg, klass):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
encoded = msg.SerializeToString()
|
||||||
|
_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)
|
||||||
|
req += encoded
|
||||||
|
await self._write(req)
|
||||||
|
|
||||||
|
async def send_message_callback_response(self, send_msg: message.Message,
|
||||||
|
on_message: Callable[[Any], None]) -> None:
|
||||||
|
self._message_handlers.append(on_message)
|
||||||
|
await self.send_message(send_msg)
|
||||||
|
|
||||||
|
async def send_message_await_response_complex(self, send_msg: message.Message,
|
||||||
|
do_append: Callable[[Any], bool],
|
||||||
|
do_stop: Callable[[Any], bool],
|
||||||
|
timeout: float = 5.0) -> List[Any]:
|
||||||
|
fut = self._params.eventloop.create_future()
|
||||||
|
responses = []
|
||||||
|
|
||||||
|
def on_message(resp):
|
||||||
|
if fut.done():
|
||||||
|
return
|
||||||
|
if do_append(resp):
|
||||||
|
responses.append(resp)
|
||||||
|
if do_stop(resp):
|
||||||
|
fut.set_result(responses)
|
||||||
|
|
||||||
|
self._message_handlers.append(on_message)
|
||||||
|
await self.send_message(send_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(fut, timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
if self._stopped:
|
||||||
|
raise APIConnectionError("Disconnected while waiting for API response!")
|
||||||
|
raise APIConnectionError("Timeout while waiting for API response!")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._message_handlers.remove(on_message)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
async def send_message_await_response(self,
|
||||||
|
send_msg: message.Message,
|
||||||
|
response_type: Any, timeout: float = 5.0) -> Any:
|
||||||
|
def is_response(msg):
|
||||||
|
return isinstance(msg, response_type)
|
||||||
|
|
||||||
|
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)))
|
||||||
|
|
||||||
|
return res[0]
|
||||||
|
|
||||||
|
async def _recv(self, amount: int) -> bytes:
|
||||||
|
if amount == 0:
|
||||||
|
return bytes()
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret = await self._socket_reader.readexactly(amount)
|
||||||
|
except (asyncio.IncompleteReadError, OSError, TimeoutError) as err:
|
||||||
|
raise APIConnectionError("Error while receiving data: {}".format(err))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def _recv_varint(self) -> int:
|
||||||
|
raw = bytes()
|
||||||
|
while not raw or raw[-1] & 0x80:
|
||||||
|
raw += await self._recv(1)
|
||||||
|
return cast(int, _bytes_to_varuint(raw))
|
||||||
|
|
||||||
|
async def _run_once(self) -> None:
|
||||||
|
preamble = await self._recv(1)
|
||||||
|
if preamble[0] != 0x00:
|
||||||
|
raise APIConnectionError("Invalid preamble")
|
||||||
|
|
||||||
|
length = await self._recv_varint()
|
||||||
|
msg_type = await self._recv_varint()
|
||||||
|
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
for msg_handler in self._message_handlers[:]:
|
||||||
|
msg_handler(msg)
|
||||||
|
await self._handle_internal_messages(msg)
|
||||||
|
|
||||||
|
async def run_forever(self) -> None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await self._run_once()
|
||||||
|
except APIConnectionError as err:
|
||||||
|
_LOGGER.info("%s: Error while reading incoming messages: %s",
|
||||||
|
self._params.address, err)
|
||||||
|
await self._on_error()
|
||||||
|
break
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.info("%s: Unexpected error while reading incoming messages: %s",
|
||||||
|
self._params.address, err)
|
||||||
|
await self._on_error()
|
||||||
|
break
|
||||||
|
|
||||||
|
async def _handle_internal_messages(self, msg: Any) -> None:
|
||||||
|
if isinstance(msg, pb.DisconnectRequest):
|
||||||
|
await self.send_message(pb.DisconnectResponse())
|
||||||
|
await self.stop(force=True)
|
||||||
|
elif isinstance(msg, pb.PingRequest):
|
||||||
|
await self.send_message(pb.PingResponse())
|
||||||
|
elif isinstance(msg, pb.GetTimeRequest):
|
||||||
|
resp = pb.GetTimeResponse()
|
||||||
|
resp.epoch_seconds = int(time.time())
|
||||||
|
await self.send_message(resp)
|
||||||
|
|
||||||
|
async def ping(self) -> None:
|
||||||
|
self._check_connected()
|
||||||
|
await self.send_message_await_response(pb.PingRequest(), pb.PingResponse)
|
||||||
|
|
||||||
|
async def _disconnect(self) -> None:
|
||||||
|
self._check_connected()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.send_message_await_response(pb.DisconnectRequest(), pb.DisconnectResponse)
|
||||||
|
except APIConnectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _check_authenticated(self) -> None:
|
||||||
|
if not self._authenticated:
|
||||||
|
raise APIConnectionError("Must login first!")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_version(self) -> Optional[APIVersion]:
|
||||||
|
return self._api_version
|
57
aioesphomeapi/core.py
Normal file
57
aioesphomeapi/core.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import aioesphomeapi.api_pb2 as pb
|
||||||
|
|
||||||
|
|
||||||
|
class APIConnectionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
MESSAGE_TYPE_TO_PROTO = {
|
||||||
|
1: pb.HelloRequest,
|
||||||
|
2: pb.HelloResponse,
|
||||||
|
3: pb.ConnectRequest,
|
||||||
|
4: pb.ConnectResponse,
|
||||||
|
5: pb.DisconnectRequest,
|
||||||
|
6: pb.DisconnectResponse,
|
||||||
|
7: pb.PingRequest,
|
||||||
|
8: pb.PingResponse,
|
||||||
|
9: pb.DeviceInfoRequest,
|
||||||
|
10: pb.DeviceInfoResponse,
|
||||||
|
11: pb.ListEntitiesRequest,
|
||||||
|
12: pb.ListEntitiesBinarySensorResponse,
|
||||||
|
13: pb.ListEntitiesCoverResponse,
|
||||||
|
14: pb.ListEntitiesFanResponse,
|
||||||
|
15: pb.ListEntitiesLightResponse,
|
||||||
|
16: pb.ListEntitiesSensorResponse,
|
||||||
|
17: pb.ListEntitiesSwitchResponse,
|
||||||
|
18: pb.ListEntitiesTextSensorResponse,
|
||||||
|
19: pb.ListEntitiesDoneResponse,
|
||||||
|
20: pb.SubscribeStatesRequest,
|
||||||
|
21: pb.BinarySensorStateResponse,
|
||||||
|
22: pb.CoverStateResponse,
|
||||||
|
23: pb.FanStateResponse,
|
||||||
|
24: pb.LightStateResponse,
|
||||||
|
25: pb.SensorStateResponse,
|
||||||
|
26: pb.SwitchStateResponse,
|
||||||
|
27: pb.TextSensorStateResponse,
|
||||||
|
28: pb.SubscribeLogsRequest,
|
||||||
|
29: pb.SubscribeLogsResponse,
|
||||||
|
30: pb.CoverCommandRequest,
|
||||||
|
31: pb.FanCommandRequest,
|
||||||
|
32: pb.LightCommandRequest,
|
||||||
|
33: pb.SwitchCommandRequest,
|
||||||
|
34: pb.SubscribeServiceCallsRequest,
|
||||||
|
35: pb.ServiceCallResponse,
|
||||||
|
36: pb.GetTimeRequest,
|
||||||
|
37: pb.GetTimeResponse,
|
||||||
|
38: pb.SubscribeHomeAssistantStatesRequest,
|
||||||
|
39: pb.SubscribeHomeAssistantStateResponse,
|
||||||
|
40: pb.HomeAssistantStateResponse,
|
||||||
|
41: pb.ListEntitiesServicesResponse,
|
||||||
|
42: pb.ExecuteServiceRequest,
|
||||||
|
43: pb.ListEntitiesCameraResponse,
|
||||||
|
44: pb.CameraImageResponse,
|
||||||
|
45: pb.CameraImageRequest,
|
||||||
|
46: pb.ListEntitiesClimateResponse,
|
||||||
|
47: pb.ClimateStateResponse,
|
||||||
|
48: pb.ClimateCommandRequest,
|
||||||
|
}
|
273
aioesphomeapi/model.py
Normal file
273
aioesphomeapi/model.py
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import enum
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(cmp=True)
|
||||||
|
class APIVersion:
|
||||||
|
major = attr.ib(type=int)
|
||||||
|
minor = attr.ib(type=int)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class DeviceInfo:
|
||||||
|
uses_password = attr.ib(type=bool)
|
||||||
|
name = attr.ib(type=str)
|
||||||
|
mac_address = attr.ib(type=str)
|
||||||
|
esphome_core_version = attr.ib(type=str)
|
||||||
|
compilation_time = attr.ib(type=str)
|
||||||
|
model = attr.ib(type=str)
|
||||||
|
has_deep_sleep = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class EntityInfo:
|
||||||
|
object_id = attr.ib(type=str)
|
||||||
|
key = attr.ib(type=int)
|
||||||
|
name = attr.ib(type=str)
|
||||||
|
unique_id = attr.ib(type=str)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class EntityState:
|
||||||
|
key = attr.ib(type=int)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== BINARY SENSOR ====================
|
||||||
|
@attr.s
|
||||||
|
class BinarySensorInfo(EntityInfo):
|
||||||
|
device_class = attr.ib(type=str)
|
||||||
|
is_status_binary_sensor = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class BinarySensorState(EntityState):
|
||||||
|
state = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== COVER ====================
|
||||||
|
@attr.s
|
||||||
|
class CoverInfo(EntityInfo):
|
||||||
|
assumed_state = attr.ib(type=bool)
|
||||||
|
supports_position = attr.ib(type=bool)
|
||||||
|
supports_tilt = attr.ib(type=bool)
|
||||||
|
device_class = attr.ib(type=str)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyCoverState(enum.IntEnum):
|
||||||
|
OPEN = 0
|
||||||
|
CLOSED = 1
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyCoverCommand(enum.IntEnum):
|
||||||
|
OPEN = 0
|
||||||
|
CLOSE = 1
|
||||||
|
STOP = 2
|
||||||
|
|
||||||
|
|
||||||
|
class CoverOperation(enum.IntEnum):
|
||||||
|
IDLE = 0
|
||||||
|
IS_OPENING = 1
|
||||||
|
IS_CLOSING = 2
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class CoverState(EntityState):
|
||||||
|
legacy_state = attr.ib(type=LegacyCoverState, converter=LegacyCoverState)
|
||||||
|
position = attr.ib(type=float)
|
||||||
|
tilt = attr.ib(type=float)
|
||||||
|
current_operation = attr.ib(type=CoverOperation, converter=CoverOperation)
|
||||||
|
|
||||||
|
def is_closed(self, api_version: APIVersion):
|
||||||
|
if api_version >= APIVersion(1, 1):
|
||||||
|
return self.position == 0.0
|
||||||
|
return self.legacy_state == LegacyCoverState.CLOSED
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== FAN ====================
|
||||||
|
@attr.s
|
||||||
|
class FanInfo(EntityInfo):
|
||||||
|
supports_oscillation = attr.ib(type=bool)
|
||||||
|
supports_speed = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
class FanSpeed(enum.IntEnum):
|
||||||
|
LOW = 0
|
||||||
|
MEDIUM = 1
|
||||||
|
HIGH = 2
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class FanState(EntityState):
|
||||||
|
state = attr.ib(type=bool)
|
||||||
|
oscillating = attr.ib(type=bool)
|
||||||
|
speed = attr.ib(type=FanSpeed, converter=FanSpeed)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== LIGHT ====================
|
||||||
|
@attr.s
|
||||||
|
class LightInfo(EntityInfo):
|
||||||
|
supports_brightness = attr.ib(type=bool)
|
||||||
|
supports_rgb = attr.ib(type=bool)
|
||||||
|
supports_white_value = attr.ib(type=bool)
|
||||||
|
supports_color_temperature = attr.ib(type=bool)
|
||||||
|
min_mireds = attr.ib(type=float)
|
||||||
|
max_mireds = attr.ib(type=float)
|
||||||
|
effects = attr.ib(type=List[str], converter=list)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class LightState(EntityState):
|
||||||
|
state = attr.ib(type=bool)
|
||||||
|
brightness = attr.ib(type=float)
|
||||||
|
red = attr.ib(type=float)
|
||||||
|
green = attr.ib(type=float)
|
||||||
|
blue = attr.ib(type=float)
|
||||||
|
white = attr.ib(type=float)
|
||||||
|
color_temperature = attr.ib(type=float)
|
||||||
|
effect = attr.ib(type=str)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== SENSOR ====================
|
||||||
|
@attr.s
|
||||||
|
class SensorInfo(EntityInfo):
|
||||||
|
icon = attr.ib(type=str)
|
||||||
|
unit_of_measurement = attr.ib(type=str)
|
||||||
|
accuracy_decimals = attr.ib(type=int)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class SensorState(EntityState):
|
||||||
|
state = attr.ib(type=float)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== SWITCH ====================
|
||||||
|
@attr.s
|
||||||
|
class SwitchInfo(EntityInfo):
|
||||||
|
icon = attr.ib(type=str)
|
||||||
|
assumed_state = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class SwitchState(EntityState):
|
||||||
|
state = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== TEXT SENSOR ====================
|
||||||
|
@attr.s
|
||||||
|
class TextSensorInfo(EntityInfo):
|
||||||
|
icon = attr.ib(type=str)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class TextSensorState(EntityState):
|
||||||
|
state = attr.ib(type=str)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== CAMERA ====================
|
||||||
|
@attr.s
|
||||||
|
class CameraInfo(EntityInfo):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class CameraState(EntityState):
|
||||||
|
image = attr.ib(type=bytes)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== CLIMATE ====================
|
||||||
|
class ClimateMode(enum.IntEnum):
|
||||||
|
OFF = 0
|
||||||
|
AUTO = 1
|
||||||
|
COOL = 2
|
||||||
|
HEAT = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_climate_modes(value):
|
||||||
|
return [ClimateMode(val) for val in value]
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class ClimateInfo(EntityInfo):
|
||||||
|
supports_current_temperature = attr.ib(type=bool)
|
||||||
|
supports_two_point_target_temperature = attr.ib(type=bool)
|
||||||
|
supported_modes = attr.ib(type=List[ClimateMode], converter=_convert_climate_modes)
|
||||||
|
visual_min_temperature = attr.ib(type=float)
|
||||||
|
visual_max_temperature = attr.ib(type=float)
|
||||||
|
visual_temperature_step = attr.ib(type=float)
|
||||||
|
supports_away = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class ClimateState(EntityState):
|
||||||
|
mode = attr.ib(type=ClimateMode, converter=ClimateMode)
|
||||||
|
current_temperature = attr.ib(type=float)
|
||||||
|
target_temperature = attr.ib(type=float)
|
||||||
|
target_temperature_low = attr.ib(type=float)
|
||||||
|
target_temperature_high = attr.ib(type=float)
|
||||||
|
away = attr.ib(type=bool)
|
||||||
|
|
||||||
|
|
||||||
|
COMPONENT_TYPE_TO_INFO = {
|
||||||
|
'binary_sensor': BinarySensorInfo,
|
||||||
|
'cover': CoverInfo,
|
||||||
|
'fan': FanInfo,
|
||||||
|
'light': LightInfo,
|
||||||
|
'sensor': SensorInfo,
|
||||||
|
'switch': SwitchInfo,
|
||||||
|
'text_sensor': TextSensorInfo,
|
||||||
|
'camera': CameraInfo,
|
||||||
|
'climate': ClimateInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== USER-DEFINED SERVICES ====================
|
||||||
|
@attr.s
|
||||||
|
class ServiceCall:
|
||||||
|
service = attr.ib(type=str)
|
||||||
|
data = attr.ib(type=Dict[str, str], converter=dict)
|
||||||
|
data_template = attr.ib(type=Dict[str, str], converter=dict)
|
||||||
|
variables = attr.ib(type=Dict[str, str], converter=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class UserServiceArgType(enum.IntEnum):
|
||||||
|
BOOL = 0
|
||||||
|
INT = 1
|
||||||
|
FLOAT = 2
|
||||||
|
STRING = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _attr_obj_from_dict(cls, **kwargs):
|
||||||
|
return cls(**{key: kwargs[key] for key in attr.fields_dict(cls)})
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class UserServiceArg:
|
||||||
|
name = attr.ib(type=str)
|
||||||
|
type_ = attr.ib(type=UserServiceArgType, converter=UserServiceArgType)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class UserService:
|
||||||
|
name = attr.ib(type=str)
|
||||||
|
key = attr.ib(type=int)
|
||||||
|
args = attr.ib(type=List[UserServiceArg], converter=list)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(dict_):
|
||||||
|
args = []
|
||||||
|
for arg in dict_.get('args', []):
|
||||||
|
args.append(_attr_obj_from_dict(UserServiceArg, **arg))
|
||||||
|
return UserService(
|
||||||
|
name=dict_.get('name', ''),
|
||||||
|
key=dict_.get('key', 0),
|
||||||
|
args=args
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'key': self.key,
|
||||||
|
'args': [attr.asdict(arg) for arg in self.args],
|
||||||
|
}
|
60
aioesphomeapi/util.py
Normal file
60
aioesphomeapi/util.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import asyncio
|
||||||
|
import socket
|
||||||
|
from typing import Optional, Tuple, Any
|
||||||
|
|
||||||
|
from aioesphomeapi.core import APIConnectionError
|
||||||
|
|
||||||
|
|
||||||
|
def _varuint_to_bytes(value: int) -> bytes:
|
||||||
|
if value <= 0x7F:
|
||||||
|
return bytes([value])
|
||||||
|
|
||||||
|
ret = bytes()
|
||||||
|
while value:
|
||||||
|
temp = value & 0x7F
|
||||||
|
value >>= 7
|
||||||
|
if value:
|
||||||
|
ret += bytes([temp | 0x80])
|
||||||
|
else:
|
||||||
|
ret += bytes([temp])
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _bytes_to_varuint(value: bytes) -> Optional[int]:
|
||||||
|
result = 0
|
||||||
|
bitpos = 0
|
||||||
|
for val in value:
|
||||||
|
result |= (val & 0x7F) << bitpos
|
||||||
|
bitpos += 7
|
||||||
|
if (val & 0x80) == 0:
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_ip_address_getaddrinfo(eventloop: asyncio.events.AbstractEventLoop,
|
||||||
|
host: str, port: int) -> Tuple[Any, ...]:
|
||||||
|
try:
|
||||||
|
res = await eventloop.getaddrinfo(host, port, family=socket.AF_INET,
|
||||||
|
proto=socket.IPPROTO_TCP)
|
||||||
|
except OSError as err:
|
||||||
|
raise APIConnectionError("Error resolving IP address: {}".format(err))
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
raise APIConnectionError("Error resolving IP address: No matches!")
|
||||||
|
|
||||||
|
_, _, _, _, sockaddr = res[0]
|
||||||
|
|
||||||
|
return sockaddr
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_ip_address(eventloop: asyncio.events.AbstractEventLoop,
|
||||||
|
host: str, port: int) -> Tuple[Any, ...]:
|
||||||
|
try:
|
||||||
|
return await resolve_ip_address_getaddrinfo(eventloop, host, port)
|
||||||
|
except APIConnectionError as err:
|
||||||
|
if host.endswith('.local'):
|
||||||
|
from aioesphomeapi.host_resolver import resolve_host
|
||||||
|
|
||||||
|
return await eventloop.run_in_executor(None, resolve_host, host), port
|
||||||
|
raise err
|
Loading…
Reference in New Issue
Block a user