Example #1
0
def test_account(country: str):
    """Test account functionalities."""
    account = DysonAccount()

    # Login failure
    with pytest.raises(DysonLoginFailure):
        account.login_email_password(EMAIL, "wrong_pass", country)
    assert account.auth_info is None
    with pytest.raises(DysonAuthRequired):
        account.devices()

    # Login succeed
    account.login_email_password(EMAIL, PASSWORD, country)
    assert account.auth_info == AUTH_INFO

    # Devices
    devices = account.devices()
    assert devices[0].active is True
    assert devices[0].serial == DEVICE1_SERIAL
    assert devices[0].name == DEVICE1_NAME
    assert devices[0].version == DEVICE1_VERSION
    assert devices[0].credential == DEVICE1_CREDENTIAL
    assert devices[0].product_type == DEVICE1_PRODUCT_TYPE
    assert devices[0].auto_update is True
    assert devices[0].new_version_available is False
    assert devices[1].active is None
    assert devices[1].serial == DEVICE2_SERIAL
    assert devices[1].name == DEVICE2_NAME
    assert devices[1].version == DEVICE2_VERSION
    assert devices[1].credential == DEVICE2_CREDENTIAL
    assert devices[1].product_type == DEVICE2_PRODUCT_TYPE
    assert devices[1].auto_update is False
    assert devices[1].new_version_available is True
Example #2
0
def test_login_email_request_too_frequently(mocked_requests: MockedRequests):
    """Test request for otp code too frequently."""
    def _handle_email_request(json: dict, auth: Optional[AuthBase],
                              **kwargs) -> Tuple[int, Optional[dict]]:
        assert auth is None
        return (429, None)

    mocked_requests.register_handler("POST", API_PATH_EMAIL_REQUEST,
                                     _handle_email_request)

    account = DysonAccount()
    with pytest.raises(DysonOTPTooFrequently):
        account.login_email_otp(EMAIL, REGION)
Example #3
0
    async def async_step_email(self, info: Optional[dict] = None):
        errors = {}
        if info is not None:
            email = info[CONF_EMAIL]
            unique_id = f"global_{email}"
            for entry in self._async_current_entries():
                if entry.unique_id == unique_id:
                    return self.async_abort(reason="already_configured")
            await self.async_set_unique_id(unique_id)
            self._abort_if_unique_id_configured()

            account = DysonAccount()
            try:
                self._verify = await self.hass.async_add_executor_job(
                    account.login_email_otp, email, self._region)
            except DysonNetworkError:
                errors["base"] = "cannot_connect"
            except DysonInvalidAccountStatus:
                errors["base"] = "email_not_registered"
            else:
                self._email = email
                return await self.async_step_email_otp()

        info = info or {}
        return self.async_show_form(
            step_id="email",
            data_schema=vol.Schema({
                vol.Required(CONF_EMAIL, default=info.get(CONF_EMAIL, "")):
                str,
            }),
            errors=errors,
        )
Example #4
0
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up Dyson from a config entry."""
    # Get devices list
    if entry.data[CONF_REGION] == "CN":
        account = DysonAccountCN(entry.data[CONF_AUTH])
    else:
        account = DysonAccount(entry.data[CONF_AUTH])
    try:
        devices = await hass.async_add_executor_job(account.devices)
    except DysonNetworkError:
        _LOGGER.error("Cannot connect to Dyson cloud service.")
        raise ConfigEntryNotReady

    for device in devices:
        hass.async_create_task(
            hass.config_entries.flow.async_init(
                DYSON_LOCAL_DOMAIN,
                context={"source": SOURCE_DISCOVERY},
                data=device,
            ))

    hass.data[DOMAIN][entry.entry_id] = {
        DATA_ACCOUNT: account,
        DATA_DEVICES: devices,
    }
    for component in PLATFORMS:
        hass.async_create_task(
            hass.config_entries.async_forward_entry_setup(entry, component))

    return True
Example #5
0
def test_account():
    """Test account functionalities."""
    account = DysonAccount()

    # Incorrect email
    with pytest.raises(DysonInvalidAccountStatus):
        account.login_email_otp("*****@*****.**", REGION)
    assert account.auth_info is None
    with pytest.raises(DysonAuthRequired):
        account.devices()

    # Incorrect password
    with pytest.raises(DysonLoginFailure):
        verify = account.login_email_otp(EMAIL, REGION)
        verify(OTP, "wrong_pass")
    assert account.auth_info is None

    # Incorrect OTP
    with pytest.raises(DysonLoginFailure):
        verify = account.login_email_otp(EMAIL, REGION)
        verify("999999", PASSWORD)
    assert account.auth_info is None

    # Login succeed
    verify = account.login_email_otp(EMAIL, REGION)
    verify(OTP, PASSWORD)
    assert account.auth_info == AUTH_INFO_BEARER

    # Devices
    devices = account.devices()
    assert devices[0].active is True
    assert devices[0].serial == DEVICE1_SERIAL
    assert devices[0].name == DEVICE1_NAME
    assert devices[0].version == DEVICE1_VERSION
    assert devices[0].credential == DEVICE1_CREDENTIAL
    assert devices[0].product_type == DEVICE1_PRODUCT_TYPE
    assert devices[0].auto_update is True
    assert devices[0].new_version_available is False
    assert devices[1].active is None
    assert devices[1].serial == DEVICE2_SERIAL
    assert devices[1].name == DEVICE2_NAME
    assert devices[1].version == DEVICE2_VERSION
    assert devices[1].credential == DEVICE2_CREDENTIAL
    assert devices[1].product_type == DEVICE2_PRODUCT_TYPE
    assert devices[1].auto_update is False
    assert devices[1].new_version_available is True
Example #6
0
def test_server_error(mocked_requests: MockedRequests):
    """Test cloud server error handling."""
    def _handler_network_error(**kwargs):
        return (500, None)

    mocked_requests.register_handler("POST", API_PATH_EMAIL_REQUEST,
                                     _handler_network_error)
    mocked_requests.register_handler("GET", API_PATH_DEVICES,
                                     _handler_network_error)

    account = DysonAccount()
    with pytest.raises(DysonServerError):
        account.login_email_otp(EMAIL, REGION)
    account = DysonAccount(AUTH_INFO)
    with pytest.raises(DysonServerError):
        account.devices()
Example #7
0
def test_network_error(mocked_requests: MockedRequests):
    """Test network error handling."""
    def _handler_network_error(**kwargs):
        raise requests.RequestException

    mocked_requests.register_handler("POST", API_PATH_EMAIL_REQUEST,
                                     _handler_network_error)
    mocked_requests.register_handler("GET", API_PATH_DEVICES,
                                     _handler_network_error)

    account = DysonAccount()
    with pytest.raises(DysonNetworkError):
        account.login_email_otp(EMAIL, REGION)
    account = DysonAccount(AUTH_INFO)
    with pytest.raises(DysonNetworkError):
        account.devices()
Example #8
0
def test_server_error(mocked_requests: MockedRequests, country: str):
    """Test cloud server error handling."""
    def _handler_network_error(**kwargs):
        return (500, None)

    mocked_requests.register_handler("POST", API_PATH_LOGIN,
                                     _handler_network_error)
    mocked_requests.register_handler("GET", API_PATH_DEVICES,
                                     _handler_network_error)

    account = DysonAccount()
    with pytest.raises(DysonServerError):
        account.login_email_password(EMAIL, PASSWORD, country)
    account = DysonAccount(AUTH_INFO)
    with pytest.raises(DysonServerError):
        account.devices()
def _query_dyson(creds: config.DysonLinkCredentials) -> List[DysonDeviceInfo]:
    """Queries Dyson's APIs for a device list.

    This function requires user interaction, to check either their mobile or email
    for a one-time password.

    Args:
      username: email address or mobile number (mobile if country is CN)
      password: login password
      country: two-letter country code for account, e.g; IE, CN

    Returns:
      list of DysonDeviceInfo
    """
    username = creds.username
    country = creds.country

    if country == 'CN':
        # Treat username like a phone number and use login_mobile_otp.
        account = DysonAccountCN()
        if not username.startswith('+86'):
            username = '******' + username

        print(
            f'Please check your mobile device ({username}) for a one-time password.'
        )
        verify_fn = account.login_mobile_otp(username)
    else:
        account = DysonAccount()
        verify_fn = account.login_email_otp(username, country)
        print(f'Please check your email ({username}) for a one-time password.')

    print()
    otp = input('Enter OTP: ')
    try:
        verify_fn(otp, creds.password)
        return account.devices()
    except DysonLoginFailure:
        print('Incorrect OTP.')
        sys.exit(-1)
Example #10
0
def test_get_cleaning_map(mocked_requests: MockedRequests, country: str):
    """Test get cleaning map from the cloud."""
    cleaning_id = "edcda2c9-5088-455e-b2ee-9422ef70afb2"
    cleaning_map = b"mocked_png_image"

    def _clean_history_handler(auth: Optional[AuthBase],
                               **kwargs) -> Tuple[int, bytes]:
        assert auth is not None
        return (0, cleaning_map)

    mocked_requests.register_handler(
        "GET",
        f"/v1/mapvisualizer/devices/{SERIAL}/map/{cleaning_id}",
        _clean_history_handler,
    )

    account = DysonAccount(AUTH_INFO)
    device = DysonCloud360Eye(account, SERIAL)
    assert device.get_cleaning_map(cleaning_id) == cleaning_map

    # Non existed map
    assert device.get_cleaning_map("another_id") is None
Example #11
0
from libdyson.exceptions import DysonOTPTooFrequently

print("Please choose your account region")
print("1: Mainland China")
print("2: Rest of the World")
region = input("Region [1/2]: ")

if region == "1":
    account = DysonAccountCN()
    mobile = input("Phone number: ")
    verify = account.login_mobile_otp(f"+86{mobile}")
    otp = input("Verification code: ")
    verify(otp)
elif region == "2":
    region = input("Region code: ")
    account = DysonAccount()
    email = input("Email: ")
    password = getpass()
    account.login_email_password(email, password, region)
else:
    print(f"Invalid input {region}")
    exit(1)

devices = account.devices()
for device in devices:
    print()
    print(f"Serial: {device.serial}")
    print(f"Name: {device.name}")
    print(f"Device Type: {device.product_type}")
    print(f"Credential: {device.credential}")
Example #12
0
def test_account_auth_info():
    """Test initialize account with auth info."""
    account = DysonAccount(AUTH_INFO)
    devices = account.devices()
    assert len(devices) == 2

    # Invalid auth
    account = DysonAccount({
        "Account": "invalid",
        "Password": "******",
    }, )
    with pytest.raises(DysonInvalidAuth):
        account.devices()

    # No auth
    account = DysonAccount()
    with pytest.raises(DysonAuthRequired):
        account.devices()
Example #13
0
def test_get_cleaning_history(mocked_requests: MockedRequests, country: str):
    """Test get cleaning history from the cloud."""
    cleaning1_id = "edcda2c9-5088-455e-b2ee-9422ef70afb2"
    cleaning2_id = "98cf2de1-190f-4e68-97b5-c57e7e0604d0"
    clean_history = {
        "TriviaMessage":
        "Your robot has cleaned 1000yds²",
        "TriviaArea":
        800.1243,
        "Entries": [
            {
                "Clean": cleaning1_id,
                "Started": "2021-02-10T17:02:00",
                "Finished": "2021-02-10T17:02:10",
                "Area": 0.00,
                "Charges": 0,
                "Type": "Immediate",
                "IsInterim": False,
            },
            {
                "Clean": cleaning2_id,
                "Started": "2021-02-09T12:10:07",
                "Finished": "2021-02-09T14:14:11",
                "Area": 34.70,
                "Charges": 1,
                "Type": "Scheduled",
                "IsInterim": True,
            },
        ],
    }

    def _clean_history_handler(auth: Optional[AuthBase],
                               **kwargs) -> Tuple[int, Optional[dict]]:
        assert auth is not None
        return (0, clean_history)

    mocked_requests.register_handler(
        "GET", f"/v1/assets/devices/{SERIAL}/cleanhistory",
        _clean_history_handler)

    account = DysonAccount(AUTH_INFO)
    device = DysonCloud360Eye(account, SERIAL)
    cleaning_tasks = device.get_cleaning_history()
    assert len(cleaning_tasks) == 2
    task = cleaning_tasks[0]
    assert task.cleaning_id == cleaning1_id
    assert task.start_time == datetime(2021, 2, 10, 17, 2, 0)
    assert task.finish_time == datetime(2021, 2, 10, 17, 2, 10)
    assert task.cleaning_time == timedelta(seconds=10)
    assert task.area == 0.0
    assert task.charges == 0
    assert task.cleaning_type == CleaningType.Immediate
    assert task.is_interim is False
    task = cleaning_tasks[1]
    assert task.cleaning_id == cleaning2_id
    assert task.start_time == datetime(2021, 2, 9, 12, 10, 7)
    assert task.finish_time == datetime(2021, 2, 9, 14, 14, 11)
    assert task.cleaning_time == timedelta(hours=2, minutes=4, seconds=4)
    assert task.area == 34.7
    assert task.charges == 1
    assert task.cleaning_type == CleaningType.Scheduled
    assert task.is_interim is True