Exemplo n.º 1
0
 def __init__(self, entity_id: str, host: str, token: str, username: str,
              password: str, country: str, name: str, should_poll: bool,
              image_config: ImageConfig, colors: Colors,
              drawables: Drawables, sizes: Sizes, texts: Texts,
              attributes: List[str], store_map_raw: bool,
              store_map_image: bool, store_map_path: str, force_api: str):
     super().__init__()
     self.entity_id = entity_id
     self.content_type = CONTENT_TYPE
     self._vacuum = RoborockVacuum(host, token)
     self._connector = XiaomiCloudConnector(username, password)
     self._status = CameraStatus.INITIALIZING
     self._device = None
     self._name = name
     self._should_poll = should_poll
     self._image_config = image_config
     self._colors = colors
     self._drawables = drawables
     self._sizes = sizes
     self._texts = texts
     self._attributes = attributes
     self._store_map_raw = store_map_raw
     self._store_map_image = store_map_image
     self._store_map_path = store_map_path
     self._forced_api = force_api
     self._used_api = None
     self._map_saved = None
     self._image = None
     self._map_data = None
     self._logged_in = False
     self._logged_in_previously = True
     self._received_map_name_previously = True
     self._country = country
Exemplo n.º 2
0
async def async_create_miio_device_and_coordinator(
    hass: HomeAssistant, entry: ConfigEntry
) -> None:
    """Set up a data coordinator and one miio device to service multiple entities."""
    model: str = entry.data[CONF_MODEL]
    host = entry.data[CONF_HOST]
    token = entry.data[CONF_TOKEN]
    name = entry.title
    device: MiioDevice | None = None
    migrate = False
    update_method = _async_update_data_default
    coordinator_class: type[DataUpdateCoordinator] = DataUpdateCoordinator

    if (
        model not in MODELS_HUMIDIFIER
        and model not in MODELS_FAN
        and model not in MODELS_VACUUM
        and not model.startswith(ROBOROCK_GENERIC)
        and not model.startswith(ROCKROBO_GENERIC)
    ):
        return

    _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])

    # Humidifiers
    if model in MODELS_HUMIDIFIER_MIOT:
        device = AirHumidifierMiot(host, token)
        migrate = True
    elif model in MODELS_HUMIDIFIER_MJJSQ:
        device = AirHumidifierMjjsq(host, token, model=model)
        migrate = True
    elif model in MODELS_HUMIDIFIER_MIIO:
        device = AirHumidifier(host, token, model=model)
        migrate = True
    # Airpurifiers and Airfresh
    elif model in MODELS_PURIFIER_MIOT:
        device = AirPurifierMiot(host, token)
    elif model.startswith("zhimi.airpurifier."):
        device = AirPurifier(host, token)
    elif model.startswith("zhimi.airfresh."):
        device = AirFresh(host, token)
    elif model == MODEL_AIRFRESH_A1:
        device = AirFreshA1(host, token)
    elif model == MODEL_AIRFRESH_T2017:
        device = AirFreshT2017(host, token)
    elif (
        model in MODELS_VACUUM
        or model.startswith(ROBOROCK_GENERIC)
        or model.startswith(ROCKROBO_GENERIC)
    ):
        device = RoborockVacuum(host, token)
        update_method = _async_update_data_vacuum
        coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData]
    # Pedestal fans
    elif model in MODEL_TO_CLASS_MAP:
        device = MODEL_TO_CLASS_MAP[model](host, token)
    elif model in MODELS_FAN_MIIO:
        device = Fan(host, token, model=model)
    else:
        _LOGGER.error(
            "Unsupported device found! Please create an issue at "
            "https://github.com/syssi/xiaomi_airpurifier/issues "
            "and provide the following data: %s",
            model,
        )
        return

    if migrate:
        # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration
        entity_registry = er.async_get(hass)
        assert entry.unique_id
        entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id)
        if entity_id:
            # This check is entities that have a platform migration only and should be removed in the future
            if (entity := entity_registry.async_get(entity_id)) and (
                migrate_entity_name := entity.name
            ):
                hass.config_entries.async_update_entry(entry, title=migrate_entity_name)
Exemplo n.º 3
0
class VacuumCamera(Camera):
    def __init__(self, entity_id: str, host: str, token: str, username: str,
                 password: str, country: str, name: str, should_poll: bool,
                 image_config: ImageConfig, colors: Colors,
                 drawables: Drawables, sizes: Sizes, texts: Texts,
                 attributes: List[str], store_map_raw: bool,
                 store_map_image: bool, store_map_path: str, force_api: str):
        super().__init__()
        self.entity_id = entity_id
        self.content_type = CONTENT_TYPE
        self._vacuum = RoborockVacuum(host, token)
        self._connector = XiaomiCloudConnector(username, password)
        self._status = CameraStatus.INITIALIZING
        self._device = None
        self._name = name
        self._should_poll = should_poll
        self._image_config = image_config
        self._colors = colors
        self._drawables = drawables
        self._sizes = sizes
        self._texts = texts
        self._attributes = attributes
        self._store_map_raw = store_map_raw
        self._store_map_image = store_map_image
        self._store_map_path = store_map_path
        self._forced_api = force_api
        self._used_api = None
        self._map_saved = None
        self._image = None
        self._map_data = None
        self._logged_in = False
        self._logged_in_previously = True
        self._received_map_name_previously = True
        self._country = country

    async def async_added_to_hass(self) -> None:
        self.async_schedule_update_ha_state(True)

    @property
    def frame_interval(self) -> float:
        return 1

    def camera_image(self,
                     width: Optional[int] = None,
                     height: Optional[int] = None) -> Optional[bytes]:
        return self._image

    @property
    def name(self) -> str:
        return self._name

    def turn_on(self):
        self._should_poll = True

    def turn_off(self):
        self._should_poll = False

    @property
    def supported_features(self) -> int:
        return SUPPORT_ON_OFF

    @property
    def extra_state_attributes(self) -> Dict[str, Any]:
        attributes = {}
        if self._map_data is not None:
            attributes.update(
                self.extract_attributes(self._map_data, self._attributes,
                                        self._country))
        if self._store_map_raw:
            attributes[ATTRIBUTE_MAP_SAVED] = self._map_saved
        if self._device is not None:
            attributes[ATTR_MODEL] = self._device.model
            attributes[ATTR_USED_API] = self._used_api
        if self._connector.two_factor_auth_url is not None:
            attributes[
                ATTR_TWO_FACTOR_AUTH] = self._connector.two_factor_auth_url
        return attributes

    @property
    def should_poll(self) -> bool:
        return self._should_poll

    @staticmethod
    def extract_attributes(map_data: MapData, attributes_to_return: List[str],
                           country) -> Dict[str, Any]:
        attributes = {}
        rooms = []
        if map_data.rooms is not None:
            rooms = dict(
                filter(lambda x: x[0] is not None,
                       ((x[0], x[1].name) for x in map_data.rooms.items())))
            if len(rooms) == 0:
                rooms = list(map_data.rooms.keys())
        for name, value in {
                ATTRIBUTE_CALIBRATION: map_data.calibration(),
                ATTRIBUTE_CHARGER: map_data.charger,
                ATTRIBUTE_CLEANED_ROOMS: map_data.cleaned_rooms,
                ATTRIBUTE_COUNTRY: country,
                ATTRIBUTE_GOTO: map_data.goto,
                ATTRIBUTE_GOTO_PATH: map_data.goto_path,
                ATTRIBUTE_GOTO_PREDICTED_PATH: map_data.predicted_path,
                ATTRIBUTE_IGNORED_OBSTACLES: map_data.ignored_obstacles,
                ATTRIBUTE_IGNORED_OBSTACLES_WITH_PHOTO:
                map_data.ignored_obstacles_with_photo,
                ATTRIBUTE_IMAGE: map_data.image,
                ATTRIBUTE_IS_EMPTY: map_data.image.is_empty,
                ATTRIBUTE_MAP_NAME: map_data.map_name,
                ATTRIBUTE_NO_GO_AREAS: map_data.no_go_areas,
                ATTRIBUTE_NO_MOPPING_AREAS: map_data.no_mopping_areas,
                ATTRIBUTE_OBSTACLES: map_data.obstacles,
                ATTRIBUTE_OBSTACLES_WITH_PHOTO: map_data.obstacles_with_photo,
                ATTRIBUTE_PATH: map_data.path,
                ATTRIBUTE_ROOM_NUMBERS: rooms,
                ATTRIBUTE_ROOMS: map_data.rooms,
                ATTRIBUTE_VACUUM_POSITION: map_data.vacuum_position,
                ATTRIBUTE_VACUUM_ROOM: map_data.vacuum_room,
                ATTRIBUTE_VACUUM_ROOM_NAME: map_data.vacuum_room_name,
                ATTRIBUTE_WALLS: map_data.walls,
                ATTRIBUTE_ZONES: map_data.zones
        }.items():
            if name in attributes_to_return:
                attributes[name] = value
        return attributes

    def update(self):
        counter = 10
        if self._status != CameraStatus.TWO_FACTOR_AUTH_REQUIRED and not self._logged_in:
            self._handle_login()
        if self._device is None and self._logged_in:
            self._handle_device()
        map_name = self._handle_map_name(counter)
        if map_name == "retry" and self._device is not None:
            self._status = CameraStatus.FAILED_TO_RETRIEVE_MAP_FROM_VACUUM
        self._received_map_name_previously = map_name != "retry"
        if self._logged_in and map_name != "retry" and self._device is not None:
            self._handle_map_data(map_name)
        else:
            _LOGGER.debug(
                "Unable to retrieve map, reasons: Logged in - %s, map name - %s, device retrieved - %s",
                self._logged_in, map_name, self._device is not None)
            self._set_map_data(
                MapDataParser.create_empty(self._colors, str(self._status)))
        self._logged_in_previously = self._logged_in

    def _handle_login(self):
        _LOGGER.debug("Logging in...")
        self._logged_in = self._connector.login()
        if self._logged_in is None:
            _LOGGER.debug("2FA required")
            self._status = CameraStatus.TWO_FACTOR_AUTH_REQUIRED
        elif self._logged_in:
            _LOGGER.debug("Logged in")
            self._status = CameraStatus.LOGGED_IN
        else:
            _LOGGER.debug("Failed to log in")
            self._status = CameraStatus.FAILED_LOGIN
            if self._logged_in_previously:
                _LOGGER.error("Unable to log in, check credentials")

    def _handle_device(self):
        _LOGGER.debug("Retrieving device info, country: %s", self._country)
        country, user_id, device_id, model = self._connector.get_device_details(
            self._vacuum.token, self._country)
        if model is not None:
            self._country = country
            _LOGGER.debug("Retrieved device model: %s", model)
            self._device = self._create_device(user_id, device_id, model)
            _LOGGER.debug("Created device, used api: %s", self._used_api)
        else:
            _LOGGER.error("Failed to retrieve model")
            self._status = CameraStatus.FAILED_TO_RETRIEVE_DEVICE

    def _handle_map_name(self, counter: int) -> str:
        map_name = "retry"
        if self._device is not None and not self._device.should_get_map_from_vacuum(
        ):
            map_name = "0"
        while map_name == "retry" and counter > 0:
            _LOGGER.debug("Retrieving map name from device")
            time.sleep(0.1)
            try:
                map_name = self._vacuum.map()[0]
                _LOGGER.debug("Map name %s", map_name)
            except OSError as exc:
                _LOGGER.error("Got OSError while fetching the state: %s", exc)
            except DeviceException as exc:
                if self._received_map_name_previously:
                    _LOGGER.warning(
                        "Got exception while fetching the state: %s", exc)
                self._received_map_name_previously = False
            finally:
                counter = counter - 1
        return map_name

    def _handle_map_data(self, map_name: str):
        _LOGGER.debug("Retrieving map from Xiaomi cloud")
        store_map_path = self._store_map_path if self._store_map_raw else None
        map_data, map_stored = self._device.get_map(map_name, self._colors,
                                                    self._drawables,
                                                    self._texts, self._sizes,
                                                    self._image_config,
                                                    store_map_path)
        if map_data is not None:
            # noinspection PyBroadException
            try:
                _LOGGER.debug("Map data retrieved")
                self._map_saved = map_stored
                if map_data.image.is_empty:
                    _LOGGER.debug("Map is empty")
                    self._status = CameraStatus.EMPTY_MAP
                    if self._map_data is None or self._map_data.image.is_empty:
                        self._set_map_data(map_data)
                else:
                    _LOGGER.debug("Map is ok")
                    self._set_map_data(map_data)
                    self._status = CameraStatus.OK
            except:
                _LOGGER.warning("Unable to parse map data")
                self._status = CameraStatus.UNABLE_TO_PARSE_MAP
        else:
            self._logged_in = False
            _LOGGER.warning("Unable to retrieve map data")
            self._status = CameraStatus.UNABLE_TO_RETRIEVE_MAP

    def _set_map_data(self, map_data: MapData):
        img_byte_arr = io.BytesIO()
        map_data.image.data.save(img_byte_arr, format='PNG')
        self._image = img_byte_arr.getvalue()
        self._map_data = map_data
        self._store_image()

    def _create_device(self, user_id: str, device_id: str,
                       model: str) -> XiaomiCloudVacuum:
        self._used_api = self._detect_api(model)
        if self._used_api == CONF_AVAILABLE_API_XIAOMI:
            return XiaomiVacuum(self._connector, self._country, user_id,
                                device_id, model)
        if self._used_api == CONF_AVAILABLE_API_VIOMI:
            return ViomiVacuum(self._connector, self._country, user_id,
                               device_id, model)
        if self._used_api == CONF_AVAILABLE_API_ROIDMI:
            return RoidmiVacuum(self._connector, self._country, user_id,
                                device_id, model)
        if self._used_api == CONF_AVAILABLE_API_DREAME:
            return DreameVacuum(self._connector, self._country, user_id,
                                device_id, model)
        return UnsupportedVacuum(self._connector, self._country, user_id,
                                 device_id, model)

    def _detect_api(self, model: str) -> Optional[str]:
        if self._forced_api is not None:
            return self._forced_api
        if model in API_EXCEPTIONS:
            return API_EXCEPTIONS[model]

        def list_contains_model(prefixes, model_to_check):
            return len(
                list(filter(lambda x: model_to_check.startswith(x),
                            prefixes))) > 0

        filtered = list(
            filter(lambda x: list_contains_model(x[1], model),
                   AVAILABLE_APIS.items()))
        if len(filtered) > 0:
            return filtered[0][0]
        return None

    def _store_image(self):
        if self._store_map_image:
            try:
                image = Image.open(io.BytesIO(self._image))
                image.save(
                    f"{self._store_map_path}/map_image_{self._device.model}.png"
                )
            except:
                _LOGGER.warning("Error while saving image")