import enum from typing import List, Dict, TypeVar, Optional, Type import attr # All fields in here should have defaults set # Home Assistant depends on these fields being constructible # with args from a previous version of Home Assistant. # The default value should *always* be the Protobuf default value # for a field (False, 0, empty string, enum with value 0, ...) _T = TypeVar('_T') class APIIntEnum(enum.IntEnum): """Base class for int enum values in API model.""" @classmethod def convert(cls: Type[_T], value: int) -> Optional[_T]: try: return cls(value) except ValueError: return None @classmethod def convert_list(cls: Type[_T], value: List[int]) -> List[_T]: ret = [] for x in value: try: ret.append(cls(x)) except ValueError: pass return ret @attr.s class APIVersion: major = attr.ib(type=int, default=0) minor = attr.ib(type=int, default=0) @attr.s class DeviceInfo: uses_password = attr.ib(type=bool, default=False) name = attr.ib(type=str, default='') mac_address = attr.ib(type=str, default='') compilation_time = attr.ib(type=str, default='') model = attr.ib(type=str, default='') has_deep_sleep = attr.ib(type=bool, default=False) esphome_version = attr.ib(type=str, default='') @attr.s class EntityInfo: object_id = attr.ib(type=str, default='') key = attr.ib(type=int, default=0) name = attr.ib(type=str, default='') unique_id = attr.ib(type=str, default='') @attr.s class EntityState: key = attr.ib(type=int, default=0) # ==================== BINARY SENSOR ==================== @attr.s class BinarySensorInfo(EntityInfo): device_class = attr.ib(type=str, default='') is_status_binary_sensor = attr.ib(type=bool, default=False) @attr.s class BinarySensorState(EntityState): state = attr.ib(type=bool, default=False) missing_state = attr.ib(type=bool, default=False) # ==================== COVER ==================== @attr.s class CoverInfo(EntityInfo): assumed_state = attr.ib(type=bool, default=False) supports_position = attr.ib(type=bool, default=False) supports_tilt = attr.ib(type=bool, default=False) device_class = attr.ib(type=str, default='') class LegacyCoverState(APIIntEnum): OPEN = 0 CLOSED = 1 class LegacyCoverCommand(APIIntEnum): OPEN = 0 CLOSE = 1 STOP = 2 class CoverOperation(APIIntEnum): IDLE = 0 IS_OPENING = 1 IS_CLOSING = 2 @attr.s class CoverState(EntityState): legacy_state = attr.ib(type=Optional[LegacyCoverState], converter=LegacyCoverState.convert, default=LegacyCoverState.OPEN) position = attr.ib(type=float, default=0.0) tilt = attr.ib(type=float, default=0.0) current_operation = attr.ib(type=Optional[CoverOperation], converter=CoverOperation.convert, default=CoverOperation.IDLE) 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, default=False) supports_speed = attr.ib(type=bool, default=False) supports_direction = attr.ib(type=bool, default=False) supported_speed_levels = attr.ib(type=int, default=0) class FanSpeed(APIIntEnum): LOW = 0 MEDIUM = 1 HIGH = 2 class FanDirection(APIIntEnum): FORWARD = 0 REVERSE = 1 @attr.s class FanState(EntityState): state = attr.ib(type=bool, default=False) oscillating = attr.ib(type=bool, default=False) speed = attr.ib(type=Optional[FanSpeed], converter=FanSpeed.convert, default=FanSpeed.LOW) speed_level = attr.ib(type=int, default=0) direction = attr.ib(type=Optional[FanDirection], converter=FanDirection.convert, default=FanDirection.FORWARD) # ==================== LIGHT ==================== @attr.s class LightInfo(EntityInfo): supports_brightness = attr.ib(type=bool, default=False) supports_rgb = attr.ib(type=bool, default=False) supports_white_value = attr.ib(type=bool, default=False) supports_color_temperature = attr.ib(type=bool, default=False) min_mireds = attr.ib(type=float, default=0.0) max_mireds = attr.ib(type=float, default=0.0) effects = attr.ib(type=List[str], converter=list, factory=list) @attr.s class LightState(EntityState): state = attr.ib(type=bool, default=False) brightness = attr.ib(type=float, default=0.0) red = attr.ib(type=float, default=0.0) green = attr.ib(type=float, default=0.0) blue = attr.ib(type=float, default=0.0) white = attr.ib(type=float, default=0.0) color_temperature = attr.ib(type=float, default=0.0) effect = attr.ib(type=str, default='') # ==================== SENSOR ==================== class SensorStateClass(APIIntEnum): NONE = 0 MEASUREMENT = 1 @attr.s class SensorInfo(EntityInfo): icon = attr.ib(type=str, default='') device_class = attr.ib(type=str, default='') unit_of_measurement = attr.ib(type=str, default='') accuracy_decimals = attr.ib(type=int, default=0) force_update = attr.ib(type=bool, default=False) state_class = attr.ib(type=Optional[SensorStateClass], converter=SensorStateClass.convert, default=SensorStateClass.NONE) @attr.s class SensorState(EntityState): state = attr.ib(type=float, default=0.0) missing_state = attr.ib(type=bool, default=False) # ==================== SWITCH ==================== @attr.s class SwitchInfo(EntityInfo): icon = attr.ib(type=str, default='') assumed_state = attr.ib(type=bool, default=False) @attr.s class SwitchState(EntityState): state = attr.ib(type=bool, default=False) # ==================== TEXT SENSOR ==================== @attr.s class TextSensorInfo(EntityInfo): icon = attr.ib(type=str, default='') @attr.s class TextSensorState(EntityState): state = attr.ib(type=str, default='') missing_state = attr.ib(type=bool, default=False) # ==================== CAMERA ==================== @attr.s class CameraInfo(EntityInfo): pass @attr.s class CameraState(EntityState): image = attr.ib(type=bytes, factory=bytes) # ==================== CLIMATE ==================== class ClimateMode(APIIntEnum): OFF = 0 AUTO = 1 COOL = 2 HEAT = 3 FAN_ONLY = 4 DRY = 5 class ClimateFanMode(APIIntEnum): ON = 0 OFF = 1 AUTO = 2 LOW = 3 MEDIUM = 4 HIGH = 5 MIDDLE = 6 FOCUS = 7 DIFFUSE = 8 class ClimateSwingMode(APIIntEnum): OFF = 0 BOTH = 1 VERTICAL = 2 HORIZONTAL = 3 class ClimateAction(APIIntEnum): OFF = 0 COOLING = 2 HEATING = 3 IDLE = 4 DRYING = 5 FAN = 6 @attr.s class ClimateInfo(EntityInfo): supports_current_temperature = attr.ib(type=bool, default=False) supports_two_point_target_temperature = attr.ib(type=bool, default=False) supported_modes = attr.ib(type=List[ClimateMode], converter=ClimateMode.convert_list, factory=list) visual_min_temperature = attr.ib(type=float, default=0.0) visual_max_temperature = attr.ib(type=float, default=0.0) visual_temperature_step = attr.ib(type=float, default=0.0) supports_away = attr.ib(type=bool, default=False) supports_action = attr.ib(type=bool, default=False) supported_fan_modes = attr.ib( type=List[ClimateFanMode], converter=ClimateFanMode.convert_list, factory=list ) supported_swing_modes = attr.ib( type=List[ClimateSwingMode], converter=ClimateSwingMode.convert_list, factory=list ) @attr.s class ClimateState(EntityState): mode = attr.ib(type=Optional[ClimateMode], converter=ClimateMode.convert, default=ClimateMode.OFF) action = attr.ib(type=Optional[ClimateAction], converter=ClimateAction.convert, default=ClimateAction.OFF) current_temperature = attr.ib(type=float, default=0.0) target_temperature = attr.ib(type=float, default=0.0) target_temperature_low = attr.ib(type=float, default=0.0) target_temperature_high = attr.ib(type=float, default=0.0) away = attr.ib(type=bool, default=False) fan_mode = attr.ib( type=Optional[ClimateFanMode], converter=ClimateFanMode.convert, default=ClimateFanMode.ON ) swing_mode = attr.ib( type=Optional[ClimateSwingMode], converter=ClimateSwingMode.convert, default=ClimateSwingMode.OFF ) 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 ==================== def _convert_homeassistant_service_map(value): return {v.key: v.value for v in value} @attr.s class HomeassistantServiceCall: service = attr.ib(type=str, default='') is_event = attr.ib(type=bool, default=False) data = attr.ib(type=Dict[str, str], converter=_convert_homeassistant_service_map, factory=dict) data_template = attr.ib(type=Dict[str, str], converter=_convert_homeassistant_service_map, factory=dict) variables = attr.ib(type=Dict[str, str], converter=_convert_homeassistant_service_map, factory=dict) class UserServiceArgType(APIIntEnum): BOOL = 0 INT = 1 FLOAT = 2 STRING = 3 BOOL_ARRAY = 4 INT_ARRAY = 5 FLOAT_ARRAY = 6 STRING_ARRAY = 7 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, default='') type_ = attr.ib(type=Optional[UserServiceArgType], converter=UserServiceArgType.convert, default=UserServiceArgType.BOOL) @attr.s class UserService: name = attr.ib(type=str, default='') key = attr.ib(type=int, default=0) args = attr.ib(type=List[UserServiceArg], converter=list, factory=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], }