Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: JohNan/pyflichub-tcpclient
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.1.4
Choose a base ref
...
head repository: JohNan/pyflichub-tcpclient
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: refs/heads/main
Choose a head ref
  • 17 commits
  • 11 files changed
  • 4 contributors

Commits on Sep 18, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    3835c32 View commit details
  2. Adds missing file

    JohNan committed Sep 18, 2023
    Copy the full SHA
    f35d4dc View commit details

Commits on Sep 19, 2023

  1. Merge pull request #1 from JohNan/johnan/network-info

    Adds network info and fixes error with multiple event in same payload
    JohNan authored Sep 19, 2023
    Copy the full SHA
    bf338d8 View commit details
  2. Rename hub info

    JohNan committed Sep 19, 2023
    Copy the full SHA
    65be026 View commit details

Commits on Sep 27, 2023

  1. Update flichub.py

    JohNan authored Sep 27, 2023
    Copy the full SHA
    824475f View commit details
  2. Update button.py

    JohNan authored Sep 27, 2023
    Copy the full SHA
    7348637 View commit details

Commits on Sep 28, 2023

  1. Merge pull request #2 from JohNan/JohNan-patch-1

    Fixes crashes when ssid is not set and battery timestamp
    JohNan authored Sep 28, 2023
    Copy the full SHA
    1e5347a View commit details

Commits on Oct 6, 2023

  1. Handle ready event

    JohNan committed Oct 6, 2023
    Copy the full SHA
    4f0a7cc View commit details
  2. Fix

    JohNan committed Oct 6, 2023
    Copy the full SHA
    dfc4852 View commit details
  3. Merge pull request #3 from JohNan/johnan/ready-event

    Handle ready event
    JohNan authored Oct 6, 2023
    Copy the full SHA
    ca3f42e View commit details

Commits on Oct 20, 2023

  1. Fetch server version from the hub + code refactor

    JohNan committed Oct 20, 2023
    Copy the full SHA
    dfaec8a View commit details
  2. Adds missing file

    JohNan committed Oct 20, 2023
    Copy the full SHA
    a1216ad View commit details
  3. Updates test requirements

    JohNan committed Oct 20, 2023
    Copy the full SHA
    a5065cf View commit details
  4. Merge pull request #4 from JohNan/johnan/server-info

    Fetch server version from the hub + code refactor
    JohNan authored Oct 20, 2023
    Copy the full SHA
    e103a12 View commit details

Commits on May 9, 2024

  1. Update button.py (#6)

    JohNan authored May 9, 2024
    Copy the full SHA
    96945e0 View commit details

Commits on Jun 19, 2024

  1. Fix: JSON parsing error on too large payloads & server_info errors in…

    … log (#7)
    
    * fix list tyoe for FlicButton
    
    * fix dict type for _data_ready
    
    * Match command server info in tcpserver to client lib (server_info -> server)
    
    * add buffer for too long packets
    
    ---------
    
    Co-authored-by: martin <[email protected]>
    blade5502 and blade5502 authored Jun 19, 2024
    Copy the full SHA
    df59302 View commit details

Commits on Sep 7, 2024

  1. Fix tcpserver for later firmware versions (#9)

    zacharee authored Sep 7, 2024
    Copy the full SHA
    91496e3 View commit details
Showing with 314 additions and 133 deletions.
  1. +2 −0 .gitignore
  2. +12 −7 main.py
  3. +9 −3 pyflichub/button.py
  4. +115 −48 pyflichub/client.py
  5. +9 −2 pyflichub/command.py
  6. +6 −2 pyflichub/event.py
  7. +62 −0 pyflichub/flichub.py
  8. +7 −0 pyflichub/server_command.py
  9. +7 −0 pyflichub/server_info.py
  10. +1 −0 requirements_test.txt
  11. +84 −71 tcpserver.js
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -141,3 +141,5 @@ dmypy.json
# Pyre type checker
.pyre/
/.tool-versions

/pyflichub/version.py
19 changes: 12 additions & 7 deletions main.py
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
from pyflichub.client import FlicHubTcpClient
from pyflichub.command import Command
from pyflichub.event import Event
from pyflichub.flichub import FlicHubInfo

logging.basicConfig(level=logging.DEBUG)

@@ -26,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())

@@ -45,12 +47,15 @@ def client_disconnected():
print(f"Client not connected after {CLIENT_READY_TIMEOUT} secs so terminating")
exit()

buttons = await client.get_buttons()
buttons: list[FlicButton] = await client.get_buttons()
for button in buttons:
print(f"Button name: {button.name} - Connected: {button.connected}")

# for button in buttons:
# print(f"Button name: {button.name} - Battery: {await client.get_battery_status(button.bdaddr)}")
network: FlicHubInfo = await client.get_hubinfo()
if network.has_wifi():
print(f"Wifi State: {network.wifi.state} - Connected: {network.wifi.connected}")
if network.has_ethernet():
print(f"Ethernet IP: {network.ethernet.ip} - Connected: {network.ethernet.connected}")


if __name__ == '__main__':
12 changes: 9 additions & 3 deletions pyflichub/button.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
class FlicButton(dict):
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,
passive_mode: bool) -> None:
super().__init__()
passive_mode: bool, battery_timestamp: datetime = None, boot_id: str = "") -> None:
self.bdaddr = bdaddr
self.serial_number = serial_number
self.color = color
@@ -16,3 +20,5 @@ def __init__(self, bdaddr: str, serial_number: str, color: str, name: str, activ
self.firmware_version = firmware_version
self.key = key
self.passive_mode = passive_mode
self.battery_timestamp = battery_timestamp
self.boot_id = boot_id
163 changes: 115 additions & 48 deletions pyflichub/client.py
Original file line number Diff line number Diff line change
@@ -2,15 +2,19 @@
import json
import logging
import time
from datetime import datetime
from functools import partial, wraps
from typing import Union

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__)

@@ -29,10 +33,11 @@ async def run(*args, loop=None, executor=None, **kwargs):


class FlicHubTcpClient(asyncio.Protocol):
buttons: [FlicButton] = []
buttons: list[FlicButton] = []
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: dict[str: Union[asyncio.Event, None]] = {}
self._transport = None
self._command_callback = command_callback
self._event_callback = event_callback
@@ -42,16 +47,17 @@ 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._buffer = None
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(
@@ -76,73 +82,111 @@ 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) -> list[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_battery_status(self, bdaddr: str):
return await self._async_send_command(f'battery;{bdaddr}')
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 _async_send_command(self, cmd: str):
def _async_send_command(self, cmd: ServerCommand):
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._transport.write(f"{cmd}\n".encode())
else:
_LOGGER.error("Connections seems to be closed.")

async def _async_send_command_and_wait_for_data(self, cmd: ServerCommand) -> Command | None:
if self._transport is not None:
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.")

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()
_LOGGER.debug('Data received: {!r}'.format(decoded_data))

if decoded_data == 'pong':
if "\n" not in decoded_data:
if not self._buffer:
_LOGGER.debug('First data fragment received: {!r}'.format(decoded_data))
self._buffer = decoded_data
else:
_LOGGER.debug('Data fragment received: {!r}'.format(decoded_data))
self._buffer += decoded_data
return

try:
msg = json.loads(decoded_data)
if 'event' in msg:
self._handle_event(Event(**msg))
if 'command' in msg:
self._handle_command(Command(**msg))
except Exception:
_LOGGER.warning('Unable to decode received data')

else:
if self._buffer:
decoded_data = self._buffer + decoded_data

_LOGGER.debug('Data received: {!r}'.format(decoded_data))
for data_part in [data_part for data_part in decoded_data.split("\n") if data_part.strip()]:
if data_part == 'pong':
pass

try:
msg = json.loads(data_part, cls=_JSONDecoder)
if 'event' in msg:
self._handle_event(Event(**msg))
if 'command' in msg:
self._handle_command(Command(**msg))
except Exception as e:
_LOGGER.warning(e, exc_info=True)
_LOGGER.warning('Unable to decode received data')

self._buffer = None

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}")
cmd.data = self.buttons
elif cmd.command == ServerCommand.HUB_INFO:
cmd.data = FlicHubInfo(**humps.decamelize(cmd.data))

if self._data_ready is not None:
self._data_ready.set()
self._data = command_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)
@@ -153,6 +197,14 @@ def _handle_event(self, event: Event):
button = self._get_button(event.button)
_LOGGER.debug(f"Button {button.name} was {event.action}")

if event.event == 'buttonConnected':
button = self._get_button(event.button)
_LOGGER.debug(f"Button {button.name} is connected")

if event.event == 'buttonReady':
button = self._get_button(event.button)
_LOGGER.debug(f"Button {button.name} is ready")

if self._event_callback is not None and button is not None:
self._event_callback(button, event)

@@ -170,3 +222,18 @@ def _check_connection(self):
msg = ""
self._transport.write(msg.encode())
self._tcp_check_timer = time.time()


class _JSONDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(
self, object_hook=self.object_hook, *args, **kwargs)

def object_hook(self, obj):
ret = {}
for key, value in obj.items():
if key in {'batteryTimestamp'}:
ret[key] = datetime.fromtimestamp(value / 1000)
else:
ret[key] = value
return ret
11 changes: 9 additions & 2 deletions pyflichub/command.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions pyflichub/event.py
Original file line number Diff line number Diff line change
@@ -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
Loading