@ -0,0 +1,744 @@ | |||
syntax = "proto3"; | |||
import "api_options.proto"; | |||
service APIConnection { | |||
rpc hello (HelloRequest) returns (HelloResponse) { | |||
option (needs_setup_connection) = false; | |||
option (needs_authentication) = false; | |||
} | |||
rpc connect (ConnectRequest) returns (ConnectResponse) { | |||
option (needs_setup_connection) = false; | |||
option (needs_authentication) = false; | |||
} | |||
rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { | |||
option (needs_setup_connection) = false; | |||
option (needs_authentication) = false; | |||
} | |||
rpc ping (PingRequest) returns (PingResponse) { | |||
option (needs_setup_connection) = false; | |||
option (needs_authentication) = false; | |||
} | |||
rpc device_info (DeviceInfoRequest) returns (DeviceInfoResponse) { | |||
option (needs_authentication) = false; | |||
} | |||
rpc list_entities (ListEntitiesRequest) returns (void) {} | |||
rpc subscribe_states (SubscribeStatesRequest) returns (void) {} | |||
rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} | |||
rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} | |||
rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} | |||
rpc get_time (GetTimeRequest) returns (GetTimeResponse) { | |||
option (needs_authentication) = false; | |||
} | |||
rpc execute_service (ExecuteServiceRequest) returns (void) {} | |||
rpc cover_command (CoverCommandRequest) returns (void) {} | |||
rpc fan_command (FanCommandRequest) returns (void) {} | |||
rpc light_command (LightCommandRequest) returns (void) {} | |||
rpc switch_command (SwitchCommandRequest) returns (void) {} | |||
rpc camera_image (CameraImageRequest) returns (void) {} | |||
rpc climate_command (ClimateCommandRequest) returns (void) {} | |||
} | |||
// ==================== BASE PACKETS ==================== | |||
// The Home Assistant protocol is structured as a simple | |||
// TCP socket with short binary messages encoded in the protocol buffers format | |||
// First, a message in this protocol has a specific format: | |||
// * VarInt denoting the size of the message object. (type is not part of this) | |||
// * VarInt denoting the type of message. | |||
// * The message object encoded as a ProtoBuf message | |||
// The connection is established in 4 steps: | |||
// * First, the client connects to the server and sends a "Hello Request" identifying itself | |||
// * The server responds with a "Hello Response" and selects the protocol version | |||
// * After receiving this message, the client attempts to authenticate itself using | |||
// the password and a "Connect Request" | |||
// * The server responds with a "Connect Response" and notifies of invalid password. | |||
// If anything in this initial process fails, the connection must immediately closed | |||
// by both sides and _no_ disconnection message is to be sent. | |||
// Message sent at the beginning of each connection | |||
// Can only be sent by the client and only at the beginning of the connection | |||
message HelloRequest { | |||
option (id) = 1; | |||
option (source) = SOURCE_CLIENT; | |||
option (no_delay) = true; | |||
// Description of client (like User Agent) | |||
// For example "Home Assistant" | |||
// Not strictly necessary to send but nice for debugging | |||
// purposes. | |||
string client_info = 1; | |||
} | |||
// Confirmation of successful connection request. | |||
// Can only be sent by the server and only at the beginning of the connection | |||
message HelloResponse { | |||
option (id) = 2; | |||
option (source) = SOURCE_SERVER; | |||
option (no_delay) = true; | |||
// 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. | |||
// Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_ | |||
// Minor is for breaking changes in individual messages - a mismatch will lead to a warning message | |||
uint32 api_version_major = 1; | |||
uint32 api_version_minor = 2; | |||
// A string identifying the server (ESP); like client info this may be empty | |||
// and only exists for debugging/logging purposes. | |||
// For example "ESPHome v1.10.0 on ESP8266" | |||
string server_info = 3; | |||
} | |||
// 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 | |||
message ConnectRequest { | |||
option (id) = 3; | |||
option (source) = SOURCE_CLIENT; | |||
option (no_delay) = true; | |||
// The password to log in with | |||
string password = 1; | |||
} | |||
// 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 | |||
message ConnectResponse { | |||
option (id) = 4; | |||
option (source) = SOURCE_SERVER; | |||
option (no_delay) = true; | |||
bool invalid_password = 1; | |||
} | |||
// Request to close the connection. | |||
// Can be sent by both the client and server | |||
message DisconnectRequest { | |||
option (id) = 5; | |||
option (source) = SOURCE_BOTH; | |||
option (no_delay) = true; | |||
// Do not close the connection before the acknowledgement arrives | |||
} | |||
message DisconnectResponse { | |||
option (id) = 6; | |||
option (source) = SOURCE_BOTH; | |||
option (no_delay) = true; | |||
// Empty - Both parties are required to close the connection after this | |||
// message has been received. | |||
} | |||
message PingRequest { | |||
option (id) = 7; | |||
option (source) = SOURCE_BOTH; | |||
// Empty | |||
} | |||
message PingResponse { | |||
option (id) = 8; | |||
option (source) = SOURCE_BOTH; | |||
// Empty | |||
} | |||
message DeviceInfoRequest { | |||
option (id) = 9; | |||
option (source) = SOURCE_CLIENT; | |||
// Empty | |||
} | |||
message DeviceInfoResponse { | |||
option (id) = 10; | |||
option (source) = SOURCE_SERVER; | |||
bool uses_password = 1; | |||
// The name of the node, given by "App.set_name()" | |||
string name = 2; | |||
// The mac address of the device. For example "AC:BC:32:89:0E:A9" | |||
string mac_address = 3; | |||
// A string describing the ESPHome version. For example "1.10.0" | |||
string esphome_version = 4; | |||
// A string describing the date of compilation, this is generated by the compiler | |||
// and therefore may not be in the same format all the time. | |||
// If the user isn't using ESPHome, this will also not be set. | |||
string compilation_time = 5; | |||
// The model of the board. For example NodeMCU | |||
string model = 6; | |||
bool has_deep_sleep = 7; | |||
} | |||
message ListEntitiesRequest { | |||
option (id) = 11; | |||
option (source) = SOURCE_CLIENT; | |||
// Empty | |||
} | |||
message ListEntitiesDoneResponse { | |||
option (id) = 19; | |||
option (source) = SOURCE_SERVER; | |||
option (no_delay) = true; | |||
// Empty | |||
} | |||
message SubscribeStatesRequest { | |||
option (id) = 20; | |||
option (source) = SOURCE_CLIENT; | |||
// Empty | |||
} | |||
// ==================== BINARY SENSOR ==================== | |||
message ListEntitiesBinarySensorResponse { | |||
option (id) = 12; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_BINARY_SENSOR"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
string device_class = 5; | |||
bool is_status_binary_sensor = 6; | |||
} | |||
message BinarySensorStateResponse { | |||
option (id) = 21; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_BINARY_SENSOR"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool state = 2; | |||
// If the binary sensor does not have a valid state yet. | |||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller | |||
bool missing_state = 3; | |||
} | |||
// ==================== COVER ==================== | |||
message ListEntitiesCoverResponse { | |||
option (id) = 13; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_COVER"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
bool assumed_state = 5; | |||
bool supports_position = 6; | |||
bool supports_tilt = 7; | |||
string device_class = 8; | |||
} | |||
enum LegacyCoverState { | |||
LEGACY_COVER_STATE_OPEN = 0; | |||
LEGACY_COVER_STATE_CLOSED = 1; | |||
} | |||
enum CoverOperation { | |||
COVER_OPERATION_IDLE = 0; | |||
COVER_OPERATION_IS_OPENING = 1; | |||
COVER_OPERATION_IS_CLOSING = 2; | |||
} | |||
message CoverStateResponse { | |||
option (id) = 22; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_COVER"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
// legacy: state has been removed in 1.13 | |||
// clients/servers must still send/accept it until the next protocol change | |||
LegacyCoverState legacy_state = 2; | |||
float position = 3; | |||
float tilt = 4; | |||
CoverOperation current_operation = 5; | |||
} | |||
enum LegacyCoverCommand { | |||
LEGACY_COVER_COMMAND_OPEN = 0; | |||
LEGACY_COVER_COMMAND_CLOSE = 1; | |||
LEGACY_COVER_COMMAND_STOP = 2; | |||
} | |||
message CoverCommandRequest { | |||
option (id) = 30; | |||
option (source) = SOURCE_CLIENT; | |||
option (ifdef) = "USE_COVER"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
// legacy: command has been removed in 1.13 | |||
// clients/servers must still send/accept it until the next protocol change | |||
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 ==================== | |||
message ListEntitiesFanResponse { | |||
option (id) = 14; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_FAN"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
bool supports_oscillation = 5; | |||
bool supports_speed = 6; | |||
} | |||
enum FanSpeed { | |||
FAN_SPEED_LOW = 0; | |||
FAN_SPEED_MEDIUM = 1; | |||
FAN_SPEED_HIGH = 2; | |||
} | |||
message FanStateResponse { | |||
option (id) = 23; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_FAN"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool state = 2; | |||
bool oscillating = 3; | |||
FanSpeed speed = 4; | |||
} | |||
message FanCommandRequest { | |||
option (id) = 31; | |||
option (source) = SOURCE_CLIENT; | |||
option (ifdef) = "USE_FAN"; | |||
option (no_delay) = true; | |||
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 ==================== | |||
message ListEntitiesLightResponse { | |||
option (id) = 15; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_LIGHT"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
bool supports_brightness = 5; | |||
bool supports_rgb = 6; | |||
bool supports_white_value = 7; | |||
bool supports_color_temperature = 8; | |||
float min_mireds = 9; | |||
float max_mireds = 10; | |||
repeated string effects = 11; | |||
} | |||
message LightStateResponse { | |||
option (id) = 24; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_LIGHT"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool state = 2; | |||
float brightness = 3; | |||
float red = 4; | |||
float green = 5; | |||
float blue = 6; | |||
float white = 7; | |||
float color_temperature = 8; | |||
string effect = 9; | |||
} | |||
message LightCommandRequest { | |||
option (id) = 32; | |||
option (source) = SOURCE_CLIENT; | |||
option (ifdef) = "USE_LIGHT"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool has_state = 2; | |||
bool state = 3; | |||
bool has_brightness = 4; | |||
float brightness = 5; | |||
bool has_rgb = 6; | |||
float red = 7; | |||
float green = 8; | |||
float blue = 9; | |||
bool has_white = 10; | |||
float white = 11; | |||
bool has_color_temperature = 12; | |||
float color_temperature = 13; | |||
bool has_transition_length = 14; | |||
uint32 transition_length = 15; | |||
bool has_flash_length = 16; | |||
uint32 flash_length = 17; | |||
bool has_effect = 18; | |||
string effect = 19; | |||
} | |||
// ==================== SENSOR ==================== | |||
message ListEntitiesSensorResponse { | |||
option (id) = 16; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_SENSOR"; | |||
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; | |||
bool force_update = 8; | |||
} | |||
message SensorStateResponse { | |||
option (id) = 25; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_SENSOR"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
float state = 2; | |||
// If the sensor does not have a valid state yet. | |||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller | |||
bool missing_state = 3; | |||
} | |||
// ==================== SWITCH ==================== | |||
message ListEntitiesSwitchResponse { | |||
option (id) = 17; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_SWITCH"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
string icon = 5; | |||
bool assumed_state = 6; | |||
} | |||
message SwitchStateResponse { | |||
option (id) = 26; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_SWITCH"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool state = 2; | |||
} | |||
message SwitchCommandRequest { | |||
option (id) = 33; | |||
option (source) = SOURCE_CLIENT; | |||
option (ifdef) = "USE_SWITCH"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool state = 2; | |||
} | |||
// ==================== TEXT SENSOR ==================== | |||
message ListEntitiesTextSensorResponse { | |||
option (id) = 18; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_TEXT_SENSOR"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
string icon = 5; | |||
} | |||
message TextSensorStateResponse { | |||
option (id) = 27; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_TEXT_SENSOR"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
string state = 2; | |||
// If the text sensor does not have a valid state yet. | |||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller | |||
bool missing_state = 3; | |||
} | |||
// ==================== SUBSCRIBE LOGS ==================== | |||
enum LogLevel { | |||
LOG_LEVEL_NONE = 0; | |||
LOG_LEVEL_ERROR = 1; | |||
LOG_LEVEL_WARN = 2; | |||
LOG_LEVEL_INFO = 3; | |||
LOG_LEVEL_DEBUG = 4; | |||
LOG_LEVEL_VERBOSE = 5; | |||
LOG_LEVEL_VERY_VERBOSE = 6; | |||
} | |||
message SubscribeLogsRequest { | |||
option (id) = 28; | |||
option (source) = SOURCE_CLIENT; | |||
LogLevel level = 1; | |||
bool dump_config = 2; | |||
} | |||
message SubscribeLogsResponse { | |||
option (id) = 29; | |||
option (source) = SOURCE_SERVER; | |||
option (log) = false; | |||
option (no_delay) = false; | |||
LogLevel level = 1; | |||
string tag = 2; | |||
string message = 3; | |||
bool send_failed = 4; | |||
} | |||
// ==================== HOMEASSISTANT.SERVICE ==================== | |||
message SubscribeHomeassistantServicesRequest { | |||
option (id) = 34; | |||
option (source) = SOURCE_CLIENT; | |||
} | |||
message HomeassistantServiceMap { | |||
string key = 1; | |||
string value = 2; | |||
} | |||
message HomeassistantServiceResponse { | |||
option (id) = 35; | |||
option (source) = SOURCE_SERVER; | |||
option (no_delay) = true; | |||
string service = 1; | |||
repeated HomeassistantServiceMap data = 2; | |||
repeated HomeassistantServiceMap data_template = 3; | |||
repeated HomeassistantServiceMap variables = 4; | |||
bool is_event = 5; | |||
} | |||
// ==================== IMPORT HOME ASSISTANT STATES ==================== | |||
// 1. Client sends SubscribeHomeAssistantStatesRequest | |||
// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async) | |||
// 3. Client sends HomeAssistantStateResponse for state changes. | |||
message SubscribeHomeAssistantStatesRequest { | |||
option (id) = 38; | |||
option (source) = SOURCE_CLIENT; | |||
} | |||
message SubscribeHomeAssistantStateResponse { | |||
option (id) = 39; | |||
option (source) = SOURCE_SERVER; | |||
string entity_id = 1; | |||
} | |||
message HomeAssistantStateResponse { | |||
option (id) = 40; | |||
option (source) = SOURCE_CLIENT; | |||
option (no_delay) = true; | |||
string entity_id = 1; | |||
string state = 2; | |||
} | |||
// ==================== IMPORT TIME ==================== | |||
message GetTimeRequest { | |||
option (id) = 36; | |||
option (source) = SOURCE_BOTH; | |||
} | |||
message GetTimeResponse { | |||
option (id) = 37; | |||
option (source) = SOURCE_BOTH; | |||
option (no_delay) = true; | |||
fixed32 epoch_seconds = 1; | |||
} | |||
// ==================== USER-DEFINES SERVICES ==================== | |||
enum ServiceArgType { | |||
SERVICE_ARG_TYPE_BOOL = 0; | |||
SERVICE_ARG_TYPE_INT = 1; | |||
SERVICE_ARG_TYPE_FLOAT = 2; | |||
SERVICE_ARG_TYPE_STRING = 3; | |||
SERVICE_ARG_TYPE_BOOL_ARRAY = 4; | |||
SERVICE_ARG_TYPE_INT_ARRAY = 5; | |||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; | |||
SERVICE_ARG_TYPE_STRING_ARRAY = 7; | |||
} | |||
message ListEntitiesServicesArgument { | |||
string name = 1; | |||
ServiceArgType type = 2; | |||
} | |||
message ListEntitiesServicesResponse { | |||
option (id) = 41; | |||
option (source) = SOURCE_SERVER; | |||
string name = 1; | |||
fixed32 key = 2; | |||
repeated ListEntitiesServicesArgument args = 3; | |||
} | |||
message ExecuteServiceArgument { | |||
bool bool_ = 1; | |||
int32 legacy_int = 2; | |||
float float_ = 3; | |||
string string_ = 4; | |||
// ESPHome 1.14 (api v1.3) make int a signed value | |||
sint32 int_ = 5; | |||
repeated bool bool_array = 6 [packed=false]; | |||
repeated sint32 int_array = 7 [packed=false]; | |||
repeated float float_array = 8 [packed=false]; | |||
repeated string string_array = 9; | |||
} | |||
message ExecuteServiceRequest { | |||
option (id) = 42; | |||
option (source) = SOURCE_CLIENT; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
repeated ExecuteServiceArgument args = 2; | |||
} | |||
// ==================== CAMERA ==================== | |||
message ListEntitiesCameraResponse { | |||
option (id) = 43; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_ESP32_CAMERA"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
} | |||
message CameraImageResponse { | |||
option (id) = 44; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_ESP32_CAMERA"; | |||
fixed32 key = 1; | |||
bytes data = 2; | |||
bool done = 3; | |||
} | |||
message CameraImageRequest { | |||
option (id) = 45; | |||
option (source) = SOURCE_CLIENT; | |||
option (ifdef) = "USE_ESP32_CAMERA"; | |||
option (no_delay) = true; | |||
bool single = 1; | |||
bool stream = 2; | |||
} | |||
// ==================== CLIMATE ==================== | |||
enum ClimateMode { | |||
CLIMATE_MODE_OFF = 0; | |||
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; | |||
// values same as mode for readability | |||
CLIMATE_ACTION_COOLING = 2; | |||
CLIMATE_ACTION_HEATING = 3; | |||
CLIMATE_ACTION_IDLE = 4; | |||
CLIMATE_ACTION_DRYING = 5; | |||
CLIMATE_ACTION_FAN = 6; | |||
} | |||
message ListEntitiesClimateResponse { | |||
option (id) = 46; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_CLIMATE"; | |||
string object_id = 1; | |||
fixed32 key = 2; | |||
string name = 3; | |||
string unique_id = 4; | |||
bool supports_current_temperature = 5; | |||
bool supports_two_point_target_temperature = 6; | |||
repeated ClimateMode supported_modes = 7; | |||
float visual_min_temperature = 8; | |||
float visual_max_temperature = 9; | |||
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; | |||
option (source) = SOURCE_SERVER; | |||
option (ifdef) = "USE_CLIMATE"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
ClimateMode mode = 2; | |||
float current_temperature = 3; | |||
float target_temperature = 4; | |||
float target_temperature_low = 5; | |||
float target_temperature_high = 6; | |||
bool away = 7; | |||
ClimateAction action = 8; | |||
ClimateFanMode fan_mode = 9; | |||
ClimateSwingMode swing_mode = 10; | |||
} | |||
message ClimateCommandRequest { | |||
option (id) = 48; | |||
option (source) = SOURCE_CLIENT; | |||
option (ifdef) = "USE_CLIMATE"; | |||
option (no_delay) = true; | |||
fixed32 key = 1; | |||
bool has_mode = 2; | |||
ClimateMode mode = 3; | |||
bool has_target_temperature = 4; | |||
float target_temperature = 5; | |||
bool has_target_temperature_low = 6; | |||
float target_temperature_low = 7; | |||
bool has_target_temperature_high = 8; | |||
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; | |||
} |
@ -0,0 +1,24 @@ | |||
syntax = "proto2"; | |||
import "google/protobuf/descriptor.proto"; | |||
enum APISourceType { | |||
SOURCE_BOTH = 0; | |||
SOURCE_SERVER = 1; | |||
SOURCE_CLIENT = 2; | |||
} | |||
message void {} | |||
extend google.protobuf.MethodOptions { | |||
optional bool needs_setup_connection = 1038 [default=true]; | |||
optional bool needs_authentication = 1039 [default=true]; | |||
} | |||
extend google.protobuf.MessageOptions { | |||
optional uint32 id = 1036 [default=0]; | |||
optional APISourceType source = 1037 [default=SOURCE_BOTH]; | |||
optional string ifdef = 1038; | |||
optional bool log = 1039 [default=true]; | |||
optional bool no_delay = 1040 [default=false]; | |||
} |
@ -0,0 +1,54 @@ | |||
package api | |||
// API request/response types. | |||
const ( | |||
UnknownType = iota | |||
HelloRequestType | |||
HelloResponseType | |||
ConnectRequestType | |||
ConnectResponseType | |||
DisconnectRequestType | |||
DisconnectResponseType | |||
PingRequestType | |||
PingResponseType | |||
DeviceInfoRequestType | |||
DeviceInfoResponseType | |||
ListEntitiesRequestType | |||
ListEntitiesBinarySensorResponseType | |||
ListEntitiesCoverResponseType | |||
ListEntitiesFanResponseType | |||
ListEntitiesLightResponseType | |||
ListEntitiesSensorResponseType | |||
ListEntitiesSwitchResponseType | |||
ListEntitiesTextSensorResponseType | |||
ListEntitiesDoneResponseType | |||
SubscribeStatesRequestType | |||
BinarySensorStateResponseType | |||
CoverStateResponseType | |||
FanStateResponseType | |||
LightStateResponseType | |||
SensorStateResponseType | |||
SwitchStateResponseType | |||
TextSensorStateResponseType | |||
SubscribeLogsRequestType | |||
SubscribeLogsResponseType | |||
CoverCommandRequestType | |||
FanCommandRequestType | |||
LightCommandRequestType | |||
SwitchCommandRequestType | |||
SubscribeHomeAssistantServicesRequestType | |||
HomeAssistantServiceResponseType | |||
GetTimeRequestType | |||
GetTimeResponseType | |||
SubscribeHomeAssistantStatesRequestType | |||
SubscribeHomeAssistantStateResponseType | |||
HomeAssistantStateResponseType | |||
ListEntitiesServicesResponseType | |||
ExecuteServiceRequestType | |||
ListEntitiesCameraResponseType | |||
CameraImageResponseType | |||
CameraImageRequestType | |||
ListEntitiesClimateResponseType | |||
ClimateStateResponseType | |||
ClimateCommandRequestType | |||
) |
@ -0,0 +1,330 @@ | |||
package api | |||
import ( | |||
"bufio" | |||
"encoding/binary" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"github.com/golang/protobuf/proto" | |||
) | |||
var messageType = map[int]interface{}{ | |||
1: HelloRequest{}, | |||
2: HelloResponse{}, | |||
3: ConnectRequest{}, | |||
4: ConnectResponse{}, | |||
5: DisconnectRequest{}, | |||
6: DisconnectResponse{}, | |||
7: PingRequest{}, | |||
8: PingResponse{}, | |||
9: DeviceInfoRequest{}, | |||
10: DeviceInfoResponse{}, | |||
11: ListEntitiesRequest{}, | |||
12: ListEntitiesBinarySensorResponse{}, | |||
13: ListEntitiesCoverResponse{}, | |||
14: ListEntitiesFanResponse{}, | |||
15: ListEntitiesLightResponse{}, | |||
16: ListEntitiesSensorResponse{}, | |||
17: ListEntitiesSwitchResponse{}, | |||
18: ListEntitiesTextSensorResponse{}, | |||
19: ListEntitiesDoneResponse{}, | |||
20: SubscribeStatesRequest{}, | |||
21: BinarySensorStateResponse{}, | |||
22: CoverStateResponse{}, | |||
23: FanStateResponse{}, | |||
24: LightStateResponse{}, | |||
25: SensorStateResponse{}, | |||
26: SwitchStateResponse{}, | |||
27: TextSensorStateResponse{}, | |||
28: SubscribeLogsRequest{}, | |||
29: SubscribeLogsResponse{}, | |||
30: CoverCommandRequest{}, | |||
31: FanCommandRequest{}, | |||
32: LightCommandRequest{}, | |||
33: SwitchCommandRequest{}, | |||
34: SubscribeHomeassistantServicesRequest{}, | |||
35: HomeassistantServiceResponse{}, | |||
36: GetTimeRequest{}, | |||
37: GetTimeResponse{}, | |||
38: SubscribeHomeAssistantStatesRequest{}, | |||
39: SubscribeHomeAssistantStateResponse{}, | |||
40: HomeAssistantStateResponse{}, | |||
41: ListEntitiesServicesResponse{}, | |||
42: ExecuteServiceRequest{}, | |||
43: ListEntitiesCameraResponse{}, | |||
44: CameraImageResponse{}, | |||
45: CameraImageRequest{}, | |||
46: ListEntitiesClimateResponse{}, | |||
47: ClimateStateResponse{}, | |||
48: ClimateCommandRequest{}, | |||
} | |||
func Marshal(message proto.Message) ([]byte, error) { | |||
encoded, err := proto.Marshal(message) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ( | |||
packed = make([]byte, len(encoded)+17) | |||
n = 1 | |||
) | |||
// Write encoded message length | |||
n += binary.PutUvarint(packed[n:], uint64(len(encoded))) | |||
// Write message type | |||
n += binary.PutUvarint(packed[n:], TypeOf(message)) | |||
// Write message | |||
copy(packed[n:], encoded) | |||
n += len(encoded) | |||
return packed[:n], nil | |||
} | |||
func ReadMessage(r *bufio.Reader) (proto.Message, error) { | |||
b, err := r.ReadByte() | |||
if err != nil { | |||
return nil, err | |||
} | |||
if b != 0x00 { | |||
return nil, errors.New("api: protocol error: expected null byte") | |||
} | |||
// Read encoded message length | |||
length, err := binary.ReadUvarint(r) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Read encoded message type | |||
kind, err := binary.ReadUvarint(r) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Read encoded message | |||
encoded := make([]byte, length) | |||
if _, err = io.ReadFull(r, encoded); err != nil { | |||
return nil, err | |||
} | |||
message := newMessage(kind) | |||
if message == nil { | |||
return nil, fmt.Errorf("api: protocol error: unknown message type %#x", kind) | |||
} | |||
if err = proto.Unmarshal(encoded, message); err != nil { | |||
return nil, err | |||
} | |||
return message, nil | |||
} | |||
func TypeOf(value interface{}) uint64 { | |||
switch value.(type) { | |||
case HelloRequest, *HelloRequest: | |||
return HelloRequestType | |||
case HelloResponse, *HelloResponse: | |||
return HelloResponseType | |||
case ConnectRequest, *ConnectRequest: | |||
return ConnectRequestType | |||
case ConnectResponse, *ConnectResponse: | |||
return ConnectResponseType | |||
case DisconnectRequest, *DisconnectRequest: | |||
return DisconnectRequestType | |||
case DisconnectResponse, *DisconnectResponse: | |||
return DisconnectResponseType | |||
case PingRequest, *PingRequest: | |||
return PingRequestType | |||
case PingResponse, *PingResponse: | |||
return PingResponseType | |||
case DeviceInfoRequest, *DeviceInfoRequest: | |||
return DeviceInfoRequestType | |||
case DeviceInfoResponse, *DeviceInfoResponse: | |||
return DeviceInfoResponseType | |||
case ListEntitiesRequest, *ListEntitiesRequest: | |||
return ListEntitiesRequestType | |||
case ListEntitiesBinarySensorResponse, *ListEntitiesBinarySensorResponse: | |||
return ListEntitiesBinarySensorResponseType | |||
case ListEntitiesCoverResponse, *ListEntitiesCoverResponse: | |||
return ListEntitiesCoverResponseType | |||
case ListEntitiesFanResponse, *ListEntitiesFanResponse: | |||
return ListEntitiesFanResponseType | |||
case ListEntitiesLightResponse, *ListEntitiesLightResponse: | |||
return ListEntitiesLightResponseType | |||
case ListEntitiesSensorResponse, *ListEntitiesSensorResponse: | |||
return ListEntitiesSensorResponseType | |||
case ListEntitiesSwitchResponse, *ListEntitiesSwitchResponse: | |||
return ListEntitiesSwitchResponseType | |||
case ListEntitiesTextSensorResponse, *ListEntitiesTextSensorResponse: | |||
return ListEntitiesTextSensorResponseType | |||
case ListEntitiesDoneResponse, *ListEntitiesDoneResponse: | |||
return ListEntitiesDoneResponseType | |||
case SubscribeStatesRequest, *SubscribeStatesRequest: | |||
return SubscribeStatesRequestType | |||
case BinarySensorStateResponse, *BinarySensorStateResponse: | |||
return BinarySensorStateResponseType | |||
case CoverStateResponse, *CoverStateResponse: | |||
return CoverStateResponseType | |||
case FanStateResponse, *FanStateResponse: | |||
return FanStateResponseType | |||
case LightStateResponse, *LightStateResponse: | |||
return LightStateResponseType | |||
case SensorStateResponse, *SensorStateResponse: | |||
return SensorStateResponseType | |||
case SwitchStateResponse, *SwitchStateResponse: | |||
return SwitchStateResponseType | |||
case TextSensorStateResponse, *TextSensorStateResponse: | |||
return TextSensorStateResponseType | |||
case SubscribeLogsRequest, *SubscribeLogsRequest: | |||
return SubscribeLogsRequestType | |||
case SubscribeLogsResponse, *SubscribeLogsResponse: | |||
return SubscribeLogsResponseType | |||
case CoverCommandRequest, *CoverCommandRequest: | |||
return CoverCommandRequestType | |||
case FanCommandRequest, *FanCommandRequest: | |||
return FanCommandRequestType | |||
case LightCommandRequest, *LightCommandRequest: | |||
return LightCommandRequestType | |||
case SwitchCommandRequest, *SwitchCommandRequest: | |||
return SwitchCommandRequestType | |||
case SubscribeHomeassistantServicesRequest, *SubscribeHomeassistantServicesRequest: | |||
return SubscribeHomeAssistantServicesRequestType | |||
case HomeassistantServiceResponse, *HomeassistantServiceResponse: | |||
return HomeAssistantServiceResponseType | |||
case GetTimeRequest, *GetTimeRequest: | |||
return GetTimeRequestType | |||
case GetTimeResponse, *GetTimeResponse: | |||
return GetTimeResponseType | |||
case SubscribeHomeAssistantStatesRequest, *SubscribeHomeAssistantStatesRequest: | |||
return SubscribeHomeAssistantStatesRequestType | |||
case SubscribeHomeAssistantStateResponse, *SubscribeHomeAssistantStateResponse: | |||
return SubscribeHomeAssistantStateResponseType | |||
case HomeAssistantStateResponse, *HomeAssistantStateResponse: | |||
return HomeAssistantStateResponseType | |||
case ListEntitiesServicesResponse, *ListEntitiesServicesResponse: | |||
return ListEntitiesServicesResponseType | |||
case ExecuteServiceRequest, *ExecuteServiceRequest: | |||
return ExecuteServiceRequestType | |||
case ListEntitiesCameraResponse, *ListEntitiesCameraResponse: | |||
return ListEntitiesCameraResponseType | |||
case CameraImageResponse, *CameraImageResponse: | |||
return CameraImageResponseType | |||
case CameraImageRequest, *CameraImageRequest: | |||
return CameraImageRequestType | |||
case ListEntitiesClimateResponse, *ListEntitiesClimateResponse: | |||
return ListEntitiesClimateResponseType | |||
case ClimateStateResponse, *ClimateStateResponse: | |||
return ClimateStateResponseType | |||
case ClimateCommandRequest, *ClimateCommandRequest: | |||
return ClimateCommandRequestType | |||
default: | |||
return UnknownType | |||
} | |||
} | |||
func newMessage(kind uint64) proto.Message { | |||
switch kind { | |||
case 1: | |||
return new(HelloRequest) | |||
case 2: | |||
return new(HelloResponse) | |||
case 3: | |||
return new(ConnectRequest) | |||
case 4: | |||
return new(ConnectResponse) | |||
case 5: | |||
return new(DisconnectRequest) | |||
case 6: | |||
return new(DisconnectResponse) | |||
case 7: | |||
return new(PingRequest) | |||
case 8: | |||
return new(PingResponse) | |||
case 9: | |||
return new(DeviceInfoRequest) | |||
case 10: | |||
return new(DeviceInfoResponse) | |||
case 11: | |||
return new(ListEntitiesRequest) | |||
case 12: | |||
return new(ListEntitiesBinarySensorResponse) | |||
case 13: | |||
return new(ListEntitiesCoverResponse) | |||
case 14: | |||
return new(ListEntitiesFanResponse) | |||
case 15: | |||
return new(ListEntitiesLightResponse) | |||
case 16: | |||
return new(ListEntitiesSensorResponse) | |||
case 17: | |||
return new(ListEntitiesSwitchResponse) | |||
case 18: | |||
return new(ListEntitiesTextSensorResponse) | |||
case 19: | |||
return new(ListEntitiesDoneResponse) | |||
case 20: | |||
return new(SubscribeStatesRequest) | |||
case 21: | |||
return new(BinarySensorStateResponse) | |||
case 22: | |||
return new(CoverStateResponse) | |||
case 23: | |||
return new(FanStateResponse) | |||
case 24: | |||
return new(LightStateResponse) | |||
case 25: | |||
return new(SensorStateResponse) | |||
case 26: | |||
return new(SwitchStateResponse) | |||
case 27: | |||
return new(TextSensorStateResponse) | |||
case 28: | |||
return new(SubscribeLogsRequest) | |||
case 29: | |||
return new(SubscribeLogsResponse) | |||
case 30: | |||
return new(CoverCommandRequest) | |||
case 31: | |||
return new(FanCommandRequest) | |||
case 32: | |||
return new(LightCommandRequest) | |||
case 33: | |||
return new(SwitchCommandRequest) | |||
case 34: | |||
return new(SubscribeHomeassistantServicesRequest) | |||
case 35: | |||
return new(HomeassistantServiceResponse) | |||
case 36: | |||
return new(GetTimeRequest) | |||
case 37: | |||
return new(GetTimeResponse) | |||
case 38: | |||
return new(SubscribeHomeAssistantStatesRequest) | |||
case 39: | |||
return new(SubscribeHomeAssistantStateResponse) | |||
case 40: | |||
return new(HomeAssistantStateResponse) | |||
case 41: | |||
return new(ListEntitiesServicesResponse) | |||
case 42: | |||
return new(ExecuteServiceRequest) | |||
case 43: | |||
return new(ListEntitiesCameraResponse) | |||
case 44: | |||
return new(CameraImageResponse) | |||
case 45: | |||
return new(CameraImageRequest) | |||
case 46: | |||
return new(ListEntitiesClimateResponse) | |||
case 47: | |||
return new(ClimateStateResponse) | |||
case 48: | |||
return new(ClimateCommandRequest) | |||
default: | |||
return nil | |||
} | |||
} |
@ -0,0 +1,24 @@ | |||
package api | |||
import ( | |||
"bufio" | |||
"bytes" | |||
"encoding/hex" | |||
"testing" | |||
) | |||
func TestReadMessage(t *testing.T) { | |||
var ( | |||
bb = bytes.NewBuffer(testHelloResponseBytes) | |||
br = bufio.NewReader(bb) | |||
) | |||
m, err := ReadMessage(br) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
t.Logf("%T: %+v", m, m) | |||
} | |||
var ( | |||
testHelloResponseBytes, _ = hex.DecodeString("001f02080110031a1963616d657261302028657370686f6d652076312e31342e3329") | |||
) |
@ -0,0 +1,132 @@ | |||
package esphome | |||
import ( | |||
"bytes" | |||
"image" | |||
"image/jpeg" | |||
"time" | |||
"github.com/golang/protobuf/proto" | |||
"maze.io/esphome/api" | |||
) | |||
// Camera is an ESP32 camera. | |||
type Camera struct { | |||
Entity | |||
} | |||
func newCamera(client *Client, entity *api.ListEntitiesCameraResponse) *Camera { | |||
return &Camera{ | |||
Entity: Entity{ | |||
Name: entity.Name, | |||
ObjectID: entity.ObjectId, | |||
UniqueID: entity.UniqueId, | |||
Key: entity.Key, | |||
client: client, | |||
}, | |||
} | |||
} | |||
// Image grabs one image frame from the camera. | |||
func (entity *Camera) Image() (image.Image, error) { | |||
if err := entity.client.sendTimeout(&api.CameraImageRequest{ | |||
Stream: true, | |||
}, entity.client.Timeout); err != nil { | |||
return nil, err | |||
} | |||
var ( | |||
in = make(chan proto.Message, 1) | |||
out = make(chan []byte) | |||
) | |||
entity.client.waitMutex.Lock() | |||
entity.client.wait[api.CameraImageResponseType] = in | |||
entity.client.waitMutex.Unlock() | |||
go func(in <-chan proto.Message, out chan []byte) { | |||
for message := range in { | |||
if message, ok := message.(*api.CameraImageResponse); ok { | |||
out <- message.Data | |||
if message.Done { | |||
close(out) | |||
return | |||
} | |||
} | |||
} | |||
}(in, out) | |||
var buffer = new(bytes.Buffer) | |||
for chunk := range out { | |||
buffer.Write(chunk) | |||
} | |||
entity.client.waitMutex.Lock() | |||
delete(entity.client.wait, api.CameraImageResponseType) | |||
entity.client.waitMutex.Unlock() | |||
return jpeg.Decode(buffer) | |||
} | |||
// Stream returns a channel with raw image frame buffers. | |||
func (entity *Camera) Stream() (<-chan *bytes.Buffer, error) { | |||
if err := entity.client.sendTimeout(&api.CameraImageRequest{ | |||
Stream: true, | |||
}, entity.client.Timeout); err != nil { | |||
return nil, err | |||
} | |||
in := make(chan proto.Message, 1) | |||
entity.client.waitMutex.Lock() | |||
entity.client.wait[api.CameraImageResponseType] = in | |||
entity.client.waitMutex.Unlock() | |||
out := make(chan *bytes.Buffer) | |||
go func(in <-chan proto.Message, out chan<- *bytes.Buffer) { | |||
var ( | |||
ticker = time.NewTicker(time.Second) | |||
buffer = new(bytes.Buffer) | |||
) | |||
defer ticker.Stop() | |||
for { | |||
select { | |||
case message := <-in: | |||
frame := message.(*api.CameraImageResponse) | |||
buffer.Write(frame.Data) | |||
if frame.Done { | |||
out <- buffer | |||
buffer = new(bytes.Buffer) | |||
} | |||
case <-ticker.C: | |||
if err := entity.client.sendTimeout(&api.CameraImageRequest{ | |||
Stream: true, | |||
}, entity.client.Timeout); err != nil { | |||
close(out) | |||
return | |||
} | |||
} | |||
} | |||
}(in, out) | |||
return out, nil | |||
} | |||
// ImageStream is like Stream, returning decoded frame images. | |||
func (entity *Camera) ImageStream() (<-chan image.Image, error) { | |||
in, err := entity.Stream() | |||
if err != nil { | |||
return nil, err | |||
} | |||
out := make(chan image.Image) | |||
go func(in <-chan *bytes.Buffer, out chan image.Image) { | |||
defer close(out) | |||
for frame := range in { | |||
if i, err := jpeg.Decode(frame); err == nil { | |||
out <- i | |||
} | |||
} | |||
}(in, out) | |||
return out, nil | |||
} |
@ -0,0 +1,515 @@ | |||
package esphome | |||
import ( | |||
"bufio" | |||
"fmt" | |||
"net" | |||
"sync" | |||
"time" | |||
"maze.io/esphome/api" | |||
proto "github.com/golang/protobuf/proto" | |||
) | |||
const ( | |||
DefaultTimeout = 10 * time.Second | |||
DefaultPort = "6053" | |||
defaultClientInfo = "maze.io go/esphome" | |||
) | |||
type Client struct { | |||
// Info identifies this device with the ESPHome node. | |||
Info string | |||
// Timeout for read and write operations. | |||
Timeout time.Duration | |||
// Clock returns the current time. | |||
Clock func() time.Time | |||
conn net.Conn | |||
br *bufio.Reader | |||
entities clientEntities | |||
err error | |||
in chan proto.Message | |||
stop chan struct{} | |||
waitMutex sync.RWMutex | |||
wait map[uint64]chan proto.Message | |||
} | |||
type clientEntities struct { | |||
binarySensor map[uint32]*BinarySensor | |||
camera map[uint32]*Camera | |||
climate map[uint32]*Climate | |||
cover map[uint32]*Cover | |||
fan map[uint32]*Fan | |||
light map[uint32]*Light | |||
sensor map[uint32]*Sensor | |||
switches map[uint32]*Switch | |||
textSensor map[uint32]*TextSensor | |||
} | |||
func newClientEntities() clientEntities { | |||
return clientEntities{ | |||
binarySensor: make(map[uint32]*BinarySensor), | |||
camera: make(map[uint32]*Camera), | |||
climate: make(map[uint32]*Climate), | |||
cover: make(map[uint32]*Cover), | |||
fan: make(map[uint32]*Fan), | |||
light: make(map[uint32]*Light), | |||
sensor: make(map[uint32]*Sensor), | |||
switches: make(map[uint32]*Switch), | |||
textSensor: make(map[uint32]*TextSensor), | |||
} | |||
} | |||
// Dial connects to ESPHome native API on the supplied TCP address. | |||
func Dial(addr string) (*Client, error) { | |||
return DialTimeout(addr, 0) | |||
} | |||
func DialTimeout(addr string, timeout time.Duration) (*Client, error) { | |||
conn, err := net.DialTimeout("tcp", addr, timeout) | |||
if err != nil { | |||
return nil, err | |||
} | |||
c := &Client{ | |||
Timeout: timeout, | |||
Info: defaultClientInfo, | |||
Clock: func() time.Time { return time.Now() }, | |||
conn: conn, | |||
br: bufio.NewReader(conn), | |||
in: make(chan proto.Message, 16), | |||
wait: make(map[uint64]chan proto.Message), | |||
stop: make(chan struct{}), | |||
entities: newClientEntities(), | |||
} | |||
go c.reader() | |||
return c, nil | |||
} | |||
func (c *Client) reader() { | |||
defer c.conn.Close() | |||
for { | |||
select { | |||
case <-c.stop: | |||
return | |||
default: | |||
if err := c.readMessage(); err != nil { | |||
c.err = err | |||
return | |||
} | |||
} | |||
} | |||
} | |||
func (c *Client) nextMessage() (proto.Message, error) { | |||
if c.err != nil { | |||
return nil, c.err | |||
} | |||
return <-c.in, nil | |||
} | |||
func (c *Client) nextMessageTimeout(timeout time.Duration) (proto.Message, error) { | |||
if c.err != nil { | |||
return nil, c.err | |||
} | |||
select { | |||
case message := <-c.in: | |||
return message, nil | |||
case <-time.After(timeout): | |||
return nil, ErrTimeout | |||
} | |||
} | |||
func (c *Client) waitFor(messageType uint64, in chan proto.Message) { | |||
c.waitMutex.Lock() | |||
{ | |||
if other, waiting := c.wait[messageType]; waiting { | |||
other <- nil | |||
close(other) | |||
} | |||
c.wait[messageType] = in | |||
} | |||
c.waitMutex.Unlock() | |||
} | |||
func (c *Client) waitDone(messageType uint64) { | |||
c.waitMutex.Lock() | |||
{ | |||
delete(c.wait, messageType) | |||
} | |||
c.waitMutex.Unlock() | |||
} | |||
func (c *Client) waitMessage(messageType uint64) proto.Message { | |||
in := make(chan proto.Message, 1) | |||
c.waitFor(messageType, in) | |||
message := <-in | |||
c.waitDone(messageType) | |||
return message | |||
} | |||
func (c *Client) waitMessageTimeout(messageType uint64, timeout time.Duration) (proto.Message, error) { | |||
in := make(chan proto.Message, 1) | |||
c.waitFor(messageType, in) | |||
defer c.waitDone(messageType) | |||
select { | |||
case message := <-in: | |||
return message, nil | |||
case <-time.After(timeout): | |||
return nil, ErrTimeout | |||
} | |||
} | |||
func (c *Client) readMessage() (err error) { | |||
var message proto.Message | |||
if message, err = api.ReadMessage(c.br); err == nil { | |||
if !c.handleInternal(message) { | |||
c.waitMutex.Lock() | |||
in, waiting := c.wait[api.TypeOf(message)] | |||
c.waitMutex.Unlock() | |||
if waiting { | |||
in <- message | |||
} else { | |||
c.in <- message | |||
} | |||
} | |||
} | |||
return | |||
} | |||
func (c *Client) handleInternal(message proto.Message) bool { | |||
switch message := message.(type) { | |||
case *api.DisconnectRequest: | |||
_ = c.sendTimeout(&api.DisconnectResponse{}, c.Timeout) | |||
c.Close() | |||
return true | |||
case *api.PingRequest: | |||
_ = c.sendTimeout(&api.PingResponse{}, c.Timeout) | |||
return true | |||
case *api.GetTimeRequest: | |||
_ = c.sendTimeout(&api.GetTimeResponse{EpochSeconds: uint32(c.Clock().Unix())}, c.Timeout) | |||
return true | |||
case *api.FanStateResponse: | |||
if _, ok := c.entities.fan[message.Key]; ok { | |||
// TODO | |||
// entity.update(message) | |||
} | |||
case *api.CoverStateResponse: | |||
if _, ok := c.entities.cover[message.Key]; ok { | |||
// TODO | |||
// entity.update(message) | |||
} | |||
case *api.LightStateResponse: | |||
if entity, ok := c.entities.light[message.Key]; ok { | |||
entity.update(message) | |||
} | |||
case *api.SensorStateResponse: | |||
if entity, ok := c.entities.sensor[message.Key]; ok { | |||
entity.update(message) | |||
} | |||
case *api.SwitchStateResponse: | |||
if entity, ok := c.entities.switches[message.Key]; ok { | |||
entity.update(message) | |||
} | |||
} | |||
return false | |||
} | |||
func (c *Client) send(message proto.Message) error { | |||
packed, err := api.Marshal(message) | |||
if err != nil { | |||
return err | |||
} | |||
if _, err = c.conn.Write(packed); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func (c *Client) sendTimeout(message proto.Message, timeout time.Duration) error { | |||
packed, err := api.Marshal(message) | |||
if err != nil { | |||
return err | |||
} | |||
if err = c.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { | |||
return err | |||
} | |||
if _, err = c.conn.Write(packed); err != nil { | |||
return err | |||
} | |||
if err = c.conn.SetWriteDeadline(time.Time{}); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func (c *Client) sendAndWaitResponse(message proto.Message, messageType uint64) (proto.Message, error) { | |||
if err := c.send(message); err != nil { | |||
return nil, err | |||
} | |||
return c.waitMessage(messageType), nil | |||
} | |||
func (c *Client) sendAndWaitResponseTimeout(message proto.Message, messageType uint64, timeout time.Duration) (proto.Message, error) { | |||
if timeout <= 0 { | |||
return c.sendAndWaitResponse(message, messageType) | |||
} | |||
if err := c.sendTimeout(message, timeout); err != nil { | |||
return nil, err | |||
} | |||
return c.waitMessageTimeout(messageType, timeout) | |||
} | |||
// Login must be called to do the initial handshake. The provided password can be empty. | |||
func (c *Client) Login(password string) error { | |||
message, err := c.sendAndWaitResponseTimeout(&api.HelloRequest{ | |||
ClientInfo: c.Info, | |||
}, api.HelloResponseType, c.Timeout) | |||
if err != nil { | |||
return err | |||
} | |||
if message, err = c.sendAndWaitResponseTimeout(&api.ConnectRequest{ | |||
Password: password, | |||
}, api.ConnectResponseType, c.Timeout); err != nil { | |||
return err | |||
} | |||
connectResponse := message.(*api.ConnectResponse) | |||
if connectResponse.InvalidPassword { | |||
return ErrPassword | |||
} | |||
// Query available entities, this allows us to map sensor/actor names to keys. | |||
entities, err := c.listEntities() | |||
if err != nil { | |||
return err | |||
} | |||
for _, item := range entities { | |||
switch item := item.(type) { | |||
case *api.ListEntitiesBinarySensorResponse: | |||
c.entities.binarySensor[item.Key] = newBinarySensor(c, item) | |||
case *api.ListEntitiesCameraResponse: | |||
c.entities.camera[item.Key] = newCamera(c, item) | |||
case *api.ListEntitiesClimateResponse: | |||
c.entities.climate[item.Key] = newClimate(c, item) | |||
case *api.ListEntitiesCoverResponse: | |||
c.entities.cover[item.Key] = newCover(c, item) | |||
case *api.ListEntitiesFanResponse: | |||
c.entities.fan[item.Key] = newFan(c, item) | |||
case *api.ListEntitiesLightResponse: | |||
c.entities.light[item.Key] = newLight(c, item) | |||
case *api.ListEntitiesSensorResponse: | |||
c.entities.sensor[item.Key] = newSensor(c, item) | |||
case *api.ListEntitiesSwitchResponse: | |||
c.entities.switches[item.Key] = newSwitch(c, item) | |||
case *api.ListEntitiesTextSensorResponse: | |||
c.entities.textSensor[item.Key] = newTextSensor(c, item) | |||
default: | |||
fmt.Printf("unknown\t%T\n", item) | |||
} | |||
} | |||
// Subscribe to states, this is also used for streaming requests. | |||
if err = c.sendTimeout(&api.SubscribeStatesRequest{}, c.Timeout); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func (c *Client) Close() error { | |||
_, err := c.sendAndWaitResponseTimeout(&api.DisconnectRequest{}, api.DisconnectResponseType, 5*time.Second) | |||
select { | |||
case c.stop <- struct{}{}: | |||
default: | |||
} | |||
return err | |||
} | |||
// DeviceInfo contains information about the ESPHome node. | |||
type DeviceInfo struct { | |||
UsesPassword bool | |||
// The name of the node, given by "App.set_name()" | |||
Name string | |||
// The mac address of the device. For example "AC:BC:32:89:0E:A9" | |||
MacAddress string | |||
// A string describing the ESPHome version. For example "1.10.0" | |||
EsphomeVersion string | |||
// A string describing the date of compilation, this is generated by the compiler | |||
// and therefore may not be in the same format all the time. | |||
// If the user isn't using ESPHome, this will also not be set. | |||
CompilationTime string | |||
// The model of the board. For example NodeMCU | |||
Model string | |||
HasDeepSleep bool | |||
} | |||