diff --git a/main.py b/main.py index df3d4b1..12dadfa 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,7 @@ logging.basicConfig(level=logging.DEBUG) CLIENT_READY_TIMEOUT = 10.0 -HOST = ('192.168.1.249', 8124) +HOST = ('192.168.1.64', 8124) def event_callback(button: FlicButton, event: Event): @@ -27,15 +27,16 @@ def command_callback(cmd: Command): async def start(): client_ready = asyncio.Event() - def client_connected(): + async def client_connected(): print("Connected!") client_ready.set() + await client.get_server_info() - def client_disconnected(): + async def client_disconnected(): print("Disconnected!") - client.on_connected = client_connected - client.on_disconnected = client_disconnected + client.async_on_connected = client_connected + client.async_on_disconnected = client_disconnected task = asyncio.create_task(client.async_connect()) @@ -56,9 +57,6 @@ def client_disconnected(): if network.has_ethernet(): print(f"Ethernet IP: {network.ethernet.ip} - Connected: {network.ethernet.connected}") - # for button in buttons: - # print(f"Button name: {button.name} - Battery: {await client.get_battery_status(button.bdaddr)}") - if __name__ == '__main__': loop = asyncio.new_event_loop() diff --git a/pyflichub/button.py b/pyflichub/button.py index 775be57..8a1fb73 100644 --- a/pyflichub/button.py +++ b/pyflichub/button.py @@ -1,6 +1,8 @@ +from dataclasses import dataclass from datetime import datetime +@dataclass class FlicButton(): def __init__(self, bdaddr: str, serial_number: str, color: str, name: str, active_disconnect: bool, connected: bool, ready: bool, battery_status: int, uuid: str, flic_version: int, firmware_version: int, key: str, diff --git a/pyflichub/client.py b/pyflichub/client.py index 0b6f881..be9dc29 100644 --- a/pyflichub/client.py +++ b/pyflichub/client.py @@ -9,10 +9,12 @@ import async_timeout import humps +from pyflichub.button import FlicButton from pyflichub.command import Command from pyflichub.event import Event -from pyflichub.button import FlicButton from pyflichub.flichub import FlicHubInfo +from pyflichub.server_command import ServerCommand +from pyflichub.server_info import ServerInfo _LOGGER = logging.getLogger(__name__) @@ -35,7 +37,7 @@ class FlicHubTcpClient(asyncio.Protocol): network: FlicHubInfo def __init__(self, ip, port, loop, timeout=1.0, reconnect_timeout=10.0, event_callback=None, command_callback=None): - self._data_ready: Union[asyncio.Event, None] = None + self._data_ready: {str: Union[asyncio.Event, None]} = {} self._transport = None self._command_callback = command_callback self._event_callback = event_callback @@ -45,16 +47,16 @@ def __init__(self, ip, port, loop, timeout=1.0, reconnect_timeout=10.0, event_ca self._tcp_disconnect_timer = time.time() self._reconnect_timeout = reconnect_timeout self._timeout = timeout - self._data = None - self.on_connected = None - self.on_disconnected = None + self._data: dict = {} self._connecting = False + self._forced_disconnect = False + self.async_on_connected = None + self.async_on_disconnected = None - async def async_connect(self): - self._connecting = True + async def _async_connect(self): """Connect to the socket.""" try: - while self._connecting: + while self._connecting and not self._forced_disconnect: _LOGGER.info("Trying to connect to %s", self._server_address) try: await asyncio.wait_for( @@ -79,33 +81,52 @@ async def async_connect(self): def disconnect(self): _LOGGER.info("Disconnected") self._connecting = False + self._forced_disconnect = True if self._transport is not None: self._transport.close() - if self.on_disconnected is not None: - self.on_disconnected() + if self.async_on_disconnected is not None: + self._loop.create_task(self.async_on_disconnected()) + + async def async_connect(self): + self._connecting = True + self._forced_disconnect = False + await self._async_connect() + + def send_command(self, cmd: ServerCommand): + return self._async_send_command(cmd) - async def connect(self): - await self._loop.create_connection(lambda: self, *self._server_address) + async def get_buttons(self) -> [FlicButton]: + command: Command = await self._async_send_command_and_wait_for_data(ServerCommand.BUTTONS) + return command.data if command is not None else [] - async def get_buttons(self): - return await self._async_send_command('buttons') + async def get_server_info(self) -> ServerInfo | None: + command: Command = await self._async_send_command_and_wait_for_data(ServerCommand.SERVER_INFO) + return command.data - async def get_hubinfo(self): - return await self._async_send_command('network') + async def get_hubinfo(self) -> FlicHubInfo | None: + command: Command = await self._async_send_command_and_wait_for_data(ServerCommand.HUB_INFO) + return command.data - async def get_battery_status(self, bdaddr: str): - return await self._async_send_command(f'battery;{bdaddr}') + def _async_send_command(self, cmd: ServerCommand): + if self._transport is not None: + self._transport.write(f"{cmd}\n".encode()) + else: + _LOGGER.error("Connections seems to be closed.") - async def _async_send_command(self, cmd: str): + async def _async_send_command_and_wait_for_data(self, cmd: ServerCommand) -> Command | None: if self._transport is not None: - self._data_ready = asyncio.Event() - self._transport.write(cmd.encode()) - with async_timeout.timeout(DATA_READY_TIMEOUT): - await self._data_ready.wait() - self._data_ready = None - return self._data + self._data_ready[cmd] = asyncio.Event() + self._transport.write(f"{cmd}\n".encode()) + try: + with async_timeout.timeout(DATA_READY_TIMEOUT): + await self._data_ready[cmd].wait() + self._data_ready[cmd] = None + return self._data[cmd] + except asyncio.TimeoutError: + _LOGGER.warning(f"Waited for '{cmd}' data for {DATA_READY_TIMEOUT} secs.") + return None else: _LOGGER.error("Connections seems to be closed.") @@ -113,8 +134,8 @@ def connection_made(self, transport): self._transport = transport _LOGGER.debug("Connection made") - if self.on_connected is not None: - self.on_connected() + if self.async_on_connected is not None: + self._loop.create_task(self.async_on_connected()) def data_received(self, data): decoded_data = data.decode() @@ -130,33 +151,27 @@ def data_received(self, data): if 'command' in msg: self._handle_command(Command(**msg)) except Exception as e: - _LOGGER.warning(e, exc_info = True) + _LOGGER.warning(e, exc_info=True) _LOGGER.warning('Unable to decode received data') - def connection_lost(self, exc): _LOGGER.info("Connection lost") + self._connecting = True self._transport = None - self._loop.create_task(self.async_connect()) + self._loop.create_task(self._async_connect()) def _handle_command(self, cmd: Command): - command_data = cmd.data - if cmd.command == 'buttons': + if cmd.command == ServerCommand.SERVER_INFO: + cmd.data = ServerInfo(**humps.decamelize(cmd.data)) + elif cmd.command == ServerCommand.BUTTONS: self.buttons = [FlicButton(**button) for button in humps.decamelize(cmd.data)] - command_data = cmd.data = self.buttons - for button in self.buttons: - _LOGGER.debug(f"Button name: {button.name} - Connected: {button.connected}") - if cmd.command == 'network': - self.network = FlicHubInfo(**humps.decamelize(cmd.data)) - command_data = cmd.data = self.network - if self.network.has_wifi(): - _LOGGER.debug(f"Wifi State: {self.network.wifi.state} - Connected: {self.network.wifi.connected}") - if self.network.has_ethernet(): - _LOGGER.debug(f"Ethernet IP: {self.network.ethernet.ip} - Connected: {self.network.ethernet.connected}") - - if self._data_ready is not None: - self._data_ready.set() - self._data = command_data + cmd.data = self.buttons + elif cmd.command == ServerCommand.HUB_INFO: + cmd.data = FlicHubInfo(**humps.decamelize(cmd.data)) + + if self._data_ready[cmd.command] is not None and cmd.data is not None: + self._data_ready[cmd.command].set() + self._data[cmd.command] = cmd if self._command_callback is not None: self._command_callback(cmd) @@ -193,6 +208,7 @@ def _check_connection(self): self._transport.write(msg.encode()) self._tcp_check_timer = time.time() + class _JSONDecoder(json.JSONDecoder): def __init__(self, *args, **kwargs): json.JSONDecoder.__init__( @@ -202,7 +218,7 @@ def object_hook(self, obj): ret = {} for key, value in obj.items(): if key in {'batteryTimestamp'}: - ret[key] = datetime.fromtimestamp(value/1000) + ret[key] = datetime.fromtimestamp(value / 1000) else: ret[key] = value - return ret \ No newline at end of file + return ret diff --git a/pyflichub/command.py b/pyflichub/command.py index 6b219d0..0578751 100644 --- a/pyflichub/command.py +++ b/pyflichub/command.py @@ -1,4 +1,11 @@ +from dataclasses import dataclass +from typing import Any + +from pyflichub.server_command import ServerCommand + + +@dataclass class Command: - def __init__(self, command: str, data: str): - self.data = data + def __init__(self, command: ServerCommand, data: Any): self.command = command + self.data = data diff --git a/pyflichub/event.py b/pyflichub/event.py index 2202de9..94dd4a5 100644 --- a/pyflichub/event.py +++ b/pyflichub/event.py @@ -1,5 +1,9 @@ +from dataclasses import dataclass + + +@dataclass class Event: def __init__(self, event: str, button: str, action: str): - self.action = action - self.button = button self.event = event + self.button = button + self.action = action diff --git a/pyflichub/flichub.py b/pyflichub/flichub.py index efda77c..3669ad0 100644 --- a/pyflichub/flichub.py +++ b/pyflichub/flichub.py @@ -1,6 +1,7 @@ -from typing import List +from dataclasses import dataclass +@dataclass class WifiInfo: state: str ssid: str @@ -11,6 +12,7 @@ def __init__(self, connected, ip, mac): self.mac = mac +@dataclass class EthernetInfo: def __init__(self, connected, ip, mac): self.connected = connected @@ -18,12 +20,14 @@ def __init__(self, connected, ip, mac): self.mac = mac +@dataclass class DhcpInfo: def __init__(self, wifi=None, ethernet=None): self.wifi = WifiInfo(**wifi) if wifi else None self.ethernet = EthernetInfo(**ethernet) if ethernet else None +@dataclass class _WifiState: def __init__(self, state, ssid): self.state = state @@ -36,6 +40,7 @@ def _decode_ssid(ssid): return ''.join(chr(byte) for byte in ssid) +@dataclass class FlicHubInfo: def __init__(self, dhcp, wifi_state=None): self._dhcp = DhcpInfo(**dhcp) @@ -55,4 +60,3 @@ def wifi(self) -> WifiInfo: @property def ethernet(self) -> EthernetInfo: return self._dhcp.ethernet - diff --git a/pyflichub/server_command.py b/pyflichub/server_command.py new file mode 100644 index 0000000..bdd20c0 --- /dev/null +++ b/pyflichub/server_command.py @@ -0,0 +1,7 @@ +from enum import StrEnum + + +class ServerCommand(StrEnum): + BUTTONS = "buttons" + SERVER_INFO = "server" + HUB_INFO = "network" diff --git a/pyflichub/server_info.py b/pyflichub/server_info.py new file mode 100644 index 0000000..1e2fc98 --- /dev/null +++ b/pyflichub/server_info.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class ServerInfo: + def __init__(self, version: str): + self.version = version diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..de1887b --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1 @@ +pytest==7.4.2 \ No newline at end of file diff --git a/tcpserver.js b/tcpserver.js index 84f2b9e..d4fb4f7 100644 --- a/tcpserver.js +++ b/tcpserver.js @@ -1,9 +1,10 @@ console.log("Made by JohNan - https://github.com/JohNan/pyflichub-tcpclient") -var network = require('network'); -var net = require('net'); -var buttons = require('buttons'); +const network = require('network'); +const net = require('net'); +const buttons = require('buttons'); const EOL = "\n"; +const VERSION = "0.1.8"; // Configuration - start const PORT = 8124; @@ -11,85 +12,109 @@ const EVENT_BUTTON = "button"; // Configuration - end net.createServer(function (socket) { - var refreshIntervalId = null; + function write(_payload) { + socket.write(JSON.stringify(_payload) + EOL) + } + + function sendButtons() { + const _buttons = buttons.getButtons(); + console.log(JSON.stringify(_buttons)) + + const payload = { + 'command': 'buttons', + 'data': _buttons + }; + + write(payload) + } + + function sendNetworkInfo() { + const _network = network.getState(); + console.log(JSON.stringify(_network)) + + const payload = { + 'command': 'network', + 'data': _network + }; + + write(payload) + } + + function sendServerInfo() { + const payload = { + 'command': 'server_info', + 'data': { + 'version': VERSION + } + }; + + write(payload) + } console.log("Connection from " + socket.remoteAddress); - var buttonConnectedHandler = function (button) { + const buttonConnectedHandler = function (button) { console.log('Button connected:' + button.bdaddr) - var response = { + const payload = { 'event': 'buttonConnected', 'button': button.bdaddr, 'action': '' } - socket.write(JSON.stringify(response)+EOL) + write(payload) }; - var buttonReadyHandler = function (button) { + const buttonReadyHandler = function (button) { console.log('Button ready:' + button.bdaddr) - var response = { + const payload = { 'event': 'buttonReady', 'button': button.bdaddr, 'action': '' } - socket.write(JSON.stringify(response)+EOL) + write(payload) }; - var buttonAddedHandler = function (button) { + const buttonAddedHandler = function (button) { console.log('Button added:' + button.bdaddr) - var response = { + const payload = { 'event': 'buttonAdded', 'button': button.bdaddr, 'action': '' } - socket.write(JSON.stringify(response)+EOL) + write(payload) }; - var buttonDownHandler = function (button) { + const buttonDownHandler = function (button) { console.log('Button clicked:' + button.bdaddr + ' - down') - var response = { + const payload = { 'event': EVENT_BUTTON, 'button': button.bdaddr, 'action': 'down' } - socket.write(JSON.stringify(response)+EOL) + write(payload) }; - var buttonUpHandler = function (button) { + const buttonUpHandler = function (button) { console.log('Button clicked:' + button.bdaddr + ' - up') - var response = { + const payload = { 'event': EVENT_BUTTON, 'button': button.bdaddr, 'action': 'up' } - socket.write(JSON.stringify(response)+EOL) + write(payload) }; - var buttonSingleOrDoubleClickOrHoldHandler = function (button) { + const buttonSingleOrDoubleClickOrHoldHandler = function (button) { const action = button.isSingleClick ? 'single' : button.isDoubleClick ? 'double' : 'hold'; console.log('Button clicked:' + button.bdaddr + ' - ' + action) - const response = { + const payload = { 'event': EVENT_BUTTON, 'button': button.bdaddr, 'action': action }; - socket.write(JSON.stringify(response)+EOL) + write(payload) }; - function sendButtons() { - const _buttons = buttons.getButtons(); - console.log(JSON.stringify(_buttons)) - - const response = { - 'command': 'buttons', - 'data': _buttons - }; - - console.log(response) - socket.write(JSON.stringify(response)+EOL) - } - buttons.on('buttonSingleOrDoubleClickOrHold', buttonSingleOrDoubleClickOrHoldHandler); buttons.on('buttonUp', buttonUpHandler); buttons.on('buttonDown', buttonDownHandler); @@ -109,53 +134,30 @@ net.createServer(function (socket) { buttons.removeListener('buttonReady', buttonReadyHandler); buttons.removeListener('buttonAdded', buttonAddedHandler); - clearInterval(refreshIntervalId); socket.destroy(); }); socket.on('data', function (data) { - const msg = data.trim() - console.log("Received message: " + msg) - - if (msg.startsWith("battery;")) { - const _bdaddr = msg.split(";")[1]; - const button = buttons.getButton(_bdaddr) - - const response = { - 'command': 'battery', - 'data': button.batteryStatus - }; - - socket.write(JSON.stringify(response)+EOL) - } - - if (msg === "buttons") { - const _buttons = buttons.getButtons(); - console.log(JSON.stringify(_buttons)) - - const response = { - 'command': 'buttons', - 'data': _buttons - }; - - socket.write(JSON.stringify(response)+EOL) - } - - if (msg === "network") { - const _network = network.getState(); - console.log(JSON.stringify(_network)) - - const response = { - 'command': 'network', - 'data': _network - }; - - socket.write(JSON.stringify(response)+EOL) - } - - if (msg === "ping") { - socket.write("pong") - } + data.trim().split(EOL).forEach(function (msg) { + console.log("Received message: " + msg) + + switch (msg) { + case "buttons": + sendButtons(); + break; + case "network": + sendNetworkInfo(); + break; + case "server": + sendServerInfo(); + break; + case "ping": + write("pong") + break; + default: + console.error("Unknown command: " + msg) + } + }); }); }).listen(PORT, function () { console.log("Opened server on port: " + PORT);