async def test_occupancy_no_bodies(bridge_uninit: Bridge):
    """Test the that the bridge initializes even if no occupancy status is returned."""
    bridge = bridge_uninit

    # unconfirmed: user says sometimes they get back a response where the body is None.
    # It's unclear if there is some other indication via StatusCode or MessageBodyType
    # that the body is missing.
    # See #61
    bridge.occupancy_group_list_result = Response(
        Header=ResponseHeader(
            StatusCode=ResponseStatus(code=200, message="OK"),
            Url="/occupancygroup",
            MessageBodyType="MultipleOccupancyGroupDefinition",
        ),
        CommuniqueType="ReadResponse",
        Body=None,
    )
    bridge.occupancy_group_subscription_data_result = Response(
        Header=ResponseHeader(
            StatusCode=ResponseStatus(code=200, message="OK"),
            Url="/occupancygroup/status",
            MessageBodyType="MultipleOccupancyGroupStatus",
        ),
        CommuniqueType="SubscribeResponse",
        Body=None,
    )

    await bridge.initialize()

    assert bridge.target.occupancy_groups == {}
async def test_is_on(bridge: Bridge):
    """Test the is_on method returns device state."""
    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/zone/1/status",
            ),
            Body={"ZoneStatus": {
                "Level": 50,
                "Zone": {
                    "href": "/zone/1"
                }
            }},
        ))
    assert bridge.target.is_on("2") is True

    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/zone/1/status",
            ),
            Body={"ZoneStatus": {
                "Level": 0,
                "Zone": {
                    "href": "/zone/1"
                }
            }},
        ))
    assert bridge.target.is_on("2") is False
Beispiel #3
0
async def test_unsolicited(pipe):
    """Test subscribing and unsubscribing unsolicited message handlers."""
    handler1_message = None
    handler2_message = None
    handler2_called = asyncio.Event()

    def handler1(response):
        nonlocal handler1_message
        handler1_message = response

    def handler2(response):
        nonlocal handler2_message
        handler2_message = response
        handler2_called.set()

    pipe.leap.subscribe_unsolicited(handler1)
    pipe.leap.subscribe_unsolicited(handler2)

    response_dict = {
        "CommuniqueType": "ReadResponse",
        "Header": {
            "StatusCode": "200 OK",
            "Url": "/test"
        },
        "Body": {
            "Index": 0
        },
    }
    response_bytes = f"{json.dumps(response_dict)}\r\n".encode("utf-8")
    pipe.test_writer.write(response_bytes)
    response = Response.from_json(response_dict)

    await asyncio.wait_for(handler2_called.wait(), 1.0)
    handler2_called.clear()

    assert handler1_message == response, "handler1 did not receive correct message"
    assert handler2_message == response, "handler2 did not receive correct message"

    pipe.leap.unsubscribe_unsolicited(handler1)

    response_dict["Body"]["Index"] = 1
    response_bytes = f"{json.dumps(response_dict)}\r\n".encode("utf-8")
    pipe.test_writer.write(response_bytes)
    response = Response.from_json(response_dict)

    await asyncio.wait_for(handler2_called.wait(), 1.0)

    assert handler1_message != response, "handler1 was not unsubscribed"
    assert handler2_message == response, "handler2 did not receive correct message"
Beispiel #4
0
async def test_subscribe_tagged_404(pipe: Pipe):
    """Test subscribing to a topic that does not exist."""
    def _handler(_: Response):
        pass

    task = asyncio.create_task(pipe.leap.subscribe("/test", _handler))

    received = json.loads((await pipe.test_reader.readline()).decode("utf-8"))

    tag = received.get("Header", {}).pop("ClientTag", None)
    response_obj = {
        "CommuniqueType": "SubscribeResponse",
        "Header": {
            "ClientTag": tag,
            "StatusCode": "404 Not Found",
            "Url": "/test"
        },
    }
    response_bytes = f"{json.dumps(response_obj)}\r\n".encode("utf-8")
    pipe.test_writer.write(response_bytes)

    result, _ = await task

    assert result == Response(
        Header=ResponseHeader(StatusCode=ResponseStatus(404, "Not Found"),
                              Url="/test"),
        CommuniqueType="SubscribeResponse",
        Body=None,
    )

    # The subscription should not be registered.
    assert {} == pipe.leap._tagged_subscriptions  # pylint: disable=protected-access
async def test_occupancy_group_status_change_notification(bridge: Bridge):
    """Test that occupancy status changes send notifications."""
    notified = False

    def notify():
        nonlocal notified
        notified = True

    bridge.target.add_occupancy_subscriber("2", notify)
    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="MultipleOccupancyGroupStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/occupancygroup/status",
            ),
            Body={
                "OccupancyGroupStatuses": [{
                    "href": "/occupancygroup/2/status",
                    "OccupancyGroup": {
                        "href": "/occupancygroup/2"
                    },
                    "OccupancyStatus": "Unoccupied",
                }]
            },
        ))
    assert notified
async def test_notifications(bridge: Bridge):
    """Test notifications are sent to subscribers."""
    notified = False

    def callback():
        nonlocal notified
        notified = True

    bridge.target.add_subscriber("2", callback)
    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/zone/1/status",
            ),
            Body={"ZoneStatus": {
                "Level": 100,
                "Zone": {
                    "href": "/zone/1"
                }
            }},
        ))
    await asyncio.wait_for(bridge.leap.requests.join(), 10)
    assert notified
Beispiel #7
0
async def test_call(pipe: Pipe):
    """Test basic call and response."""
    task = asyncio.create_task(pipe.leap.request("ReadRequest", "/test"))

    received = json.loads((await pipe.test_reader.readline()).decode("utf-8"))

    # message should contain ClientTag
    tag = received.get("Header", {}).pop("ClientTag", None)
    assert tag
    assert isinstance(tag, str)

    assert received == {
        "CommuniqueType": "ReadRequest",
        "Header": {
            "Url": "/test"
        }
    }

    response_obj = {
        "CommuniqueType": "ReadResponse",
        "Header": {
            "ClientTag": tag,
            "StatusCode": "200 OK",
            "Url": "/test"
        },
        "Body": {
            "ok": True
        },
    }
    response_bytes = f"{json.dumps(response_obj)}\r\n".encode("utf-8")
    pipe.test_writer.write(response_bytes)

    result = await task

    assert result == Response(
        Header=ResponseHeader(StatusCode=ResponseStatus(200, "OK"),
                              Url="/test"),
        CommuniqueType="ReadResponse",
        Body={"ok": True},
    )
async def test_occupancy_group_status_change(bridge: Bridge):
    """Test that the status is updated when occupancy changes."""
    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="MultipleOccupancyGroupStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/occupancygroup/status",
            ),
            Body={
                "OccupancyGroupStatuses": [{
                    "href": "/occupancygroup/2/status",
                    "OccupancyGroup": {
                        "href": "/occupancygroup/2"
                    },
                    "OccupancyStatus": "Unoccupied",
                }]
            },
        ))
    new_status = bridge.target.occupancy_groups["2"]["status"]
    assert new_status == OCCUPANCY_GROUP_UNOCCUPIED
Beispiel #9
0
async def test_raise_cover(bridge: Bridge, event_loop):
    """Test that raising a cover produces the right commands."""
    devices = bridge.target.get_devices()
    task = event_loop.create_task(bridge.target.raise_cover("7"))
    command, response = await bridge.leap.requests.get()
    assert command == Request(
        communique_type="CreateRequest",
        url="/zone/6/commandprocessor",
        body={"Command": {"CommandType": "Raise"}},
    )
    # the real response probably contains more data
    response.set_result(
        Response(
            CommuniqueType="CreateResponse",
            Header=ResponseHeader(
                StatusCode=ResponseStatus(201, "Created"),
                Url="/zone/6/commandprocessor",
            ),
        ),
    )
    bridge.leap.requests.task_done()
    await task
    assert devices["7"]["current_state"] == 100
Beispiel #10
0
async def test_subscribe_tagged(pipe: Pipe):
    """
    Test subscribing to a topic and receiving responses.

    Unlike with unsolicited subscriptions, when the client sends a SubscribeRequest,
    the server sends back all events related to that subscription with the same tag
    value.
    """
    handler_message = None
    handler_called = asyncio.Event()

    def handler(response):
        nonlocal handler_message
        handler_message = response
        handler_called.set()

    task = asyncio.create_task(pipe.leap.subscribe("/test", handler))

    received = json.loads((await pipe.test_reader.readline()).decode("utf-8"))

    # message should contain ClientTag
    tag = received.get("Header", {}).pop("ClientTag", None)
    assert tag
    assert isinstance(tag, str)

    assert received == {
        "CommuniqueType": "SubscribeRequest",
        "Header": {
            "Url": "/test"
        },
    }

    response_obj = {
        "CommuniqueType": "SubscribeResponse",
        "Header": {
            "ClientTag": tag,
            "StatusCode": "200 OK",
            "Url": "/test"
        },
        "Body": {
            "ok": True
        },
    }
    response_bytes = f"{json.dumps(response_obj)}\r\n".encode("utf-8")
    pipe.test_writer.write(response_bytes)

    result, received_tag = await task

    assert result == Response(
        Header=ResponseHeader(StatusCode=ResponseStatus(200, "OK"),
                              Url="/test"),
        CommuniqueType="SubscribeResponse",
        Body={"ok": True},
    )
    assert received_tag == tag

    # Now that the client has subscribed, send an event to the handler.
    response_obj = {
        "CommuniqueType": "ReadResponse",
        "Header": {
            "ClientTag": tag,
            "StatusCode": "200 OK",
            "Url": "/test"
        },
        "Body": {
            "ok": True
        },
    }
    response_bytes = f"{json.dumps(response_obj)}\r\n".encode("utf-8")
    pipe.test_writer.write(response_bytes)

    await asyncio.wait_for(handler_called.wait(), 1.0)
    assert handler_message == Response(
        Header=ResponseHeader(StatusCode=ResponseStatus(200, "OK"),
                              Url="/test"),
        CommuniqueType="ReadResponse",
        Body={"ok": True},
    )
async def test_set_value(bridge: Bridge, event_loop):
    """Test that setting values produces the right commands."""
    task = event_loop.create_task(bridge.target.set_value("2", 50))
    command, response = await bridge.leap.requests.get()
    assert command == Request(
        communique_type="CreateRequest",
        url="/zone/1/commandprocessor",
        body={
            "Command": {
                "CommandType": "GoToLevel",
                "Parameter": [{
                    "Type": "Level",
                    "Value": 50
                }],
            }
        },
    )
    response.set_result(
        Response(
            CommuniqueType="CreateResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(201, "Created"),
                Url="/zone/1/commandprocessor",
            ),
            Body={
                "ZoneStatus": {
                    "href": "/zone/1/status",
                    "Level": 50,
                    "Zone": {
                        "href": "/zone/1"
                    },
                }
            },
        ))
    bridge.leap.requests.task_done()
    await task

    task = event_loop.create_task(bridge.target.turn_on("2"))
    command, response = await bridge.leap.requests.get()
    assert command == Request(
        communique_type="CreateRequest",
        url="/zone/1/commandprocessor",
        body={
            "Command": {
                "CommandType": "GoToLevel",
                "Parameter": [{
                    "Type": "Level",
                    "Value": 100
                }],
            }
        },
    )
    response.set_result(
        Response(
            CommuniqueType="CreateResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(201, "Created"),
                Url="/zone/1/commandprocessor",
            ),
            Body={
                "ZoneStatus": {
                    "href": "/zone/1/status",
                    "Level": 100,
                    "Zone": {
                        "href": "/zone/1"
                    },
                }
            },
        ), )
    bridge.leap.requests.task_done()
    await task

    task = event_loop.create_task(bridge.target.turn_off("2"))
    command, response = await bridge.leap.requests.get()
    assert command == Request(
        communique_type="CreateRequest",
        url="/zone/1/commandprocessor",
        body={
            "Command": {
                "CommandType": "GoToLevel",
                "Parameter": [{
                    "Type": "Level",
                    "Value": 0
                }],
            }
        },
    )
    response.set_result(
        Response(
            CommuniqueType="CreateResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(201, "Created"),
                Url="/zone/1/commandprocessor",
            ),
            Body={
                "ZoneStatus": {
                    "href": "/zone/1/status",
                    "Level": 0,
                    "Zone": {
                        "href": "/zone/1"
                    },
                }
            },
        ), )
    bridge.leap.requests.task_done()
    await task
def response_from_json_file(filename: str) -> Response:
    """Fetch a response from a saved JSON file."""
    responsedir = os.path.join(os.path.split(__file__)[0], "responses")
    with open(os.path.join(responsedir, filename), "r") as ifh:
        return Response.from_json(json.load(ifh))
async def test_device_list(bridge: Bridge):
    """Test methods getting devices."""
    devices = bridge.target.get_devices()
    expected_devices = {
        "1": {
            "device_id": "1",
            "name": "Smart Bridge",
            "type": "SmartBridge",
            "zone": None,
            "current_state": -1,
            "fan_speed": None,
            "model": "L-BDG2-WH",
            "serial": 1234,
        },
        "2": {
            "device_id": "2",
            "name": "Hallway_Lights",
            "type": "WallDimmer",
            "zone": "1",
            "model": "PD-6WCL-XX",
            "serial": 2345,
            "current_state": 0,
            "fan_speed": None,
        },
        "3": {
            "device_id": "3",
            "name": "Hallway_Fan",
            "type": "CasetaFanSpeedController",
            "zone": "2",
            "model": "PD-FSQN-XX",
            "serial": 3456,
            "current_state": 0,
            "fan_speed": None,
        },
        "4": {
            "device_id": "4",
            "name": "Living Room_Occupancy Sensor",
            "type": "RPSOccupancySensor",
            "model": "LRF2-XXXXB-P-XX",
            "serial": 4567,
            "current_state": -1,
            "fan_speed": None,
            "zone": None,
        },
        "5": {
            "device_id": "5",
            "name": "Master Bathroom_Occupancy Sensor Door",
            "type": "RPSOccupancySensor",
            "model": "PD-VSENS-XX",
            "serial": 5678,
            "current_state": -1,
            "fan_speed": None,
            "zone": None,
        },
        "6": {
            "device_id": "6",
            "name": "Master Bathroom_Occupancy Sensor Tub",
            "type": "RPSOccupancySensor",
            "model": "PD-OSENS-XX",
            "serial": 6789,
            "current_state": -1,
            "fan_speed": None,
            "zone": None,
        },
        "7": {
            "device_id": "7",
            "name": "Living Room_Living Shade 3",
            "type": "QsWirelessShade",
            "model": "QSYC-J-RCVR",
            "serial": 1234,
            "current_state": 0,
            "fan_speed": None,
            "zone": "6",
        },
    }

    assert devices == expected_devices

    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/zone/1/status",
            ),
            Body={"ZoneStatus": {
                "Level": 100,
                "Zone": {
                    "href": "/zone/1"
                }
            }},
        ))
    bridge.leap.send_unsolicited(
        Response(
            CommuniqueType="ReadResponse",
            Header=ResponseHeader(
                MessageBodyType="OneZoneStatus",
                StatusCode=ResponseStatus(200, "OK"),
                Url="/zone/2/status",
            ),
            Body={
                "ZoneStatus": {
                    "FanSpeed": "Medium",
                    "Zone": {
                        "href": "/zone/2"
                    }
                }
            },
        ))
    devices = bridge.target.get_devices()
    assert devices["2"]["current_state"] == 100
    assert devices["2"]["fan_speed"] is None
    assert devices["3"]["current_state"] == -1
    assert devices["3"]["fan_speed"] == FAN_MEDIUM

    devices = bridge.target.get_devices_by_domain("light")
    assert len(devices) == 1
    assert devices[0]["device_id"] == "2"

    devices = bridge.target.get_devices_by_type("WallDimmer")
    assert len(devices) == 1
    assert devices[0]["device_id"] == "2"

    devices = bridge.target.get_devices_by_types(("SmartBridge", "WallDimmer"))
    assert len(devices) == 2

    device = bridge.target.get_device_by_id("2")
    assert device["device_id"] == "2"

    devices = bridge.target.get_devices_by_domain("fan")
    assert len(devices) == 1
    assert devices[0]["device_id"] == "3"

    devices = bridge.target.get_devices_by_type("CasetaFanSpeedController")
    assert len(devices) == 1
    assert devices[0]["device_id"] == "3"
    async def _accept_connection(self, leap, wait):
        """Accept a connection from SmartBridge (implementation)."""
        # First message should be read request on /device
        request, response = await wait(leap.requests.get())
        assert request == Request(communique_type="ReadRequest", url="/device")
        response.set_result(response_from_json_file("devices.json"))
        leap.requests.task_done()

        # Second message should be read request on /virtualbutton
        request, response = await wait(leap.requests.get())
        assert request == Request(communique_type="ReadRequest",
                                  url="/virtualbutton")
        response.set_result(response_from_json_file("scenes.json"))
        leap.requests.task_done()

        # Third message should be read request on /areas
        request, response = await wait(leap.requests.get())
        assert request == Request(communique_type="ReadRequest", url="/area")
        response.set_result(response_from_json_file("areas.json"))
        leap.requests.task_done()

        # Fourth message should be read request on /occupancygroup
        request, response = await wait(leap.requests.get())
        assert request == Request(communique_type="ReadRequest",
                                  url="/occupancygroup")
        response.set_result(response_from_json_file("occupancygroups.json"))
        leap.requests.task_done()

        # Fifth message should be subscribe request on /occupancygroup/status
        request, response = await wait(leap.requests.get())
        assert request == Request(communique_type="SubscribeRequest",
                                  url="/occupancygroup/status")
        response.set_result(
            response_from_json_file("occupancygroupsubscribe.json"))
        leap.requests.task_done()

        # Finally, we should check the zone status on each zone
        requested_zones = []
        for _ in range(0, 3):
            request, response = await wait(leap.requests.get())
            logging.info("Read %s", request)
            assert request.communique_type == "ReadRequest"
            requested_zones.append(request.url)
            response.set_result(
                Response(
                    CommuniqueType="ReadResponse",
                    Header=ResponseHeader(
                        MessageBodyType="OneZoneStatus",
                        StatusCode=ResponseStatus(200, "OK"),
                        Url=request.url,
                    ),
                    Body={
                        "ZoneStatus": {
                            "href": request.url,
                            "Level": 0,
                            "Zone": {
                                "href": request.url.replace("/status", "")
                            },
                            "StatusAccuracy": "Good",
                        }
                    },
                ))
            leap.requests.task_done()
        requested_zones.sort()
        assert requested_zones == [
            "/zone/1/status", "/zone/2/status", "/zone/6/status"
        ]