diff --git a/custom_components/badconga/app/client.py b/custom_components/badconga/app/client.py index a3af852..93fad5c 100644 --- a/custom_components/badconga/app/client.py +++ b/custom_components/badconga/app/client.py @@ -25,6 +25,7 @@ def __init__(self): self.password = None self.session_id = None self.timer = None + self.pongs = -1 self.device = Device() self.builder = Builder() self.map = Map() @@ -38,6 +39,7 @@ def __init__(self): 'SMSG_SESSION_LOGIN': self.handle_session_login, 'SMSG_DEVICE_STATUS': self.handle_device_status, 'SMSG_DEVICE_LIST': self.handle_device_list, + 'SMSG_DEVICE_BUSY': self.handle_device_busy, 'SMSG_USER_KICK': self.handle_user_kick, 'SMSG_PING': self.handle_ping, 'SMSG_DISCONNECT_DEVICE': self.handle_disconnect_device, @@ -84,17 +86,16 @@ def on_disconnect(self): def handle_ping(self, _): """ handlePing """ - timer = self.timer - if timer: - timer.cancel() - timer = threading.Timer(30.0, self.ping) - timer.start() - self.timer = timer + self.pongs += 1 + + # request the map only after the device reponded to a ping + if not self.map.robot.isvalid(): + self.get_map_info() def handle_user_login(self, schema): """ handle_user_login """ if schema.result != 0: - raise Exception('user login error ({})'.format(hex(schema.result))) + raise Exception(f'user login error ({ hex(schema.result) })') if schema.body.deviceId == 0: raise Exception('device not configured on this account') self.set_session( @@ -107,11 +108,12 @@ def handle_user_login(self, schema): def handle_session_login(self, schema): """ handle_session_login """ if schema.result != 0: - raise Exception('session login error ({})'.format(hex(schema.result))) + raise Exception(f'session login error ({ hex(schema.result) })') self.trigger('login') def handle_device_status(self, schema): """ handle_device_status """ + self.pongs += 1 # count status message as ping reply, too self.device.battery_level = schema.battery self.device.work_mode = schema.workMode self.device.charge_status = schema.chargeStatus @@ -125,7 +127,7 @@ def handle_device_status(self, schema): def handle_device_list(self, schema): """ handle_device_list """ if schema.result != 0: - raise Exception('device list error ({})'.format(hex(schema.result))) + raise Exception(f'device list error ({ hex(schema.result) })') if schema.body.deviceList.deviceId == 0: raise Exception('device not configured on this account') self.device.serial_number = schema.body.deviceList.serialNumber @@ -136,6 +138,10 @@ def handle_device_list(self, schema): self.device.controller_version = schema.body.deviceList.ctrlVersion self.device.model = schema.body.deviceList.deviceType + def handle_device_busy(self, schema): + """ handle_device_busy """ + self.device.busy_result = schema.result + def handle_user_kick(self, schema): """ handle_user_kick """ logger.info('Logout: %s', schema.reason) @@ -167,9 +173,14 @@ def handle_map_update(self, schema): self.map.charger.x = schema.robotChargeInfo.poseX self.map.charger.y = schema.robotChargeInfo.poseY self.map.charger.phi = schema.robotChargeInfo.posePhi - self.map.robot.x = schema.robotPoseInfo.poseX - self.map.robot.y = schema.robotPoseInfo.poseY - self.map.robot.phi = schema.robotPoseInfo.posePhi + if schema.statusInfo.workingMode not in ( + 1, # cleaning + ): + # only apply the position if the robot is docked. + # here, it is not updated while cleaning. + self.map.robot.x = schema.robotPoseInfo.poseX + self.map.robot.y = schema.robotPoseInfo.poseY + self.map.robot.phi = schema.robotPoseInfo.posePhi self.map.invalidate() self.trigger('update_map') @@ -191,8 +202,7 @@ def handle_user_logout(self, _): def on_login(self): """ on_login """ self.get_device_list() - self.get_map_info() - self.ping() + self.ping(first=True) # methods @@ -215,10 +225,23 @@ def disconnect_device(self): """ disconnect_device """ self.send('CMSG_DISCONNECT_DEVICE') - def ping(self): + def ping(self, first=False): """ ping """ + if self.pongs < 1 and not first: + logger.debug('no response to ping, reconnecting') + self.socket.disconnect() + return + + self.pongs = 0 self.send('CMSG_PING') + timer = self.timer + if timer: + timer.cancel() + timer = threading.Timer(30.0, self.ping) + timer.start() + self.timer = timer + def set_session(self, session_id, user_id, device_id): """ set_session """ self.builder.user_id = user_id diff --git a/custom_components/badconga/app/const.py b/custom_components/badconga/app/const.py index ac9f72a..2defe6a 100644 --- a/custom_components/badconga/app/const.py +++ b/custom_components/badconga/app/const.py @@ -130,3 +130,25 @@ MODEL_NAME = { MODEL_TYPE_CONGA_5090: 'Conga 5090' } + +STATUS_TYPES = { + 1: "Error", + 2: "Attention", + 3: "Information", +} + +STATUS_CODES = { + 501: "Wheels suspended", + 503: "Dust bin not installed", + 514: "Vacuum stuck", + 2102: "Cleaning finished, returning to dock", + 2103: "Charging", + 2104: "Cleaning aborted, returning to dock", + 2105: "Fully charged", + 2108: "2108", # seen on 5090 + 2110: "Orienting", +} + +BUSY_CODES = { + 2: "New map", +} diff --git a/custom_components/badconga/app/entities.py b/custom_components/badconga/app/entities.py index fa59443..016bc24 100644 --- a/custom_components/badconga/app/entities.py +++ b/custom_components/badconga/app/entities.py @@ -54,6 +54,7 @@ def __init__(self): self.clean_size = None self.type = 0 self.fault_code = None + self.busy_result = False self.fan_mode: FanMode = FAN_MODE_UNKNOWN self.serial_number = None self.utc_registered = None diff --git a/custom_components/badconga/app/map.py b/custom_components/badconga/app/map.py index 341dca3..67bef08 100644 --- a/custom_components/badconga/app/map.py +++ b/custom_components/badconga/app/map.py @@ -109,16 +109,20 @@ def draw_elements(self, map_image): fill=pixel_charger, outline=pixel_charger, width=1) if self.robot.isvalid(): pos = self.get_position((self.robot.x, self.robot.y)) - if self.robot.phi == 0.0: - aperture = 0.1 + if self.robot.phi == 0.0 or not self.animate: + # most of the times the heading is wrong if it is 0.0 + draw.ellipse(calc_ellipse(self.get_position((self.robot.x, + self.robot.y)), + 3), + fill=pixel_robot, outline=pixel_robot, width=1) else: - step = self.frame % len(steps) if self.animate else 0 + step = self.frame % len(steps) aperture = steps[step] - start, end = calc_segment(math.degrees(self.robot.phi), - aperture) - draw.pieslice(calc_ellipse(pos, 3), - start, end, - fill=pixel_robot, outline=pixel_robot, width=1) + start, end = calc_segment(math.degrees(self.robot.phi), + aperture) + draw.pieslice(calc_ellipse(pos, 3), + start, end, + fill=pixel_robot, outline=pixel_robot, width=1) return map_image def invalidate(self): diff --git a/custom_components/badconga/app/opcode_handlers.py b/custom_components/badconga/app/opcode_handlers.py index eb3febe..008f670 100644 --- a/custom_components/badconga/app/opcode_handlers.py +++ b/custom_components/badconga/app/opcode_handlers.py @@ -37,7 +37,7 @@ def read_string(data): """ read_string """ (str_len,) = struct.unpack('=b', data.read(1)) if str_len: - (string,) = struct.unpack('{}s'.format(str_len), data.read(str_len)) + (string,) = struct.unpack(f'{ str_len }s', data.read(str_len)) return string.decode('utf-8') return '' @@ -69,8 +69,8 @@ def read_area_info_list(parent, data): message = parent.add() message.CopyFrom(dict_to_message(schema_pb2.AreaInfo(), (area_id, area_type))) if points: - message.x.extend(struct.unpack('{}f'.format(points), data.read(points * 4))) - message.y.extend(struct.unpack('{}f'.format(points), data.read(points * 4))) + message.x.extend(struct.unpack(f'{ points }f', data.read(points * 4))) + message.y.extend(struct.unpack(f'{ points }f', data.read(points * 4))) data.read(points * 3 * 4) # dump values def read_clean_room_info_list(parent, data): diff --git a/custom_components/badconga/camera.py b/custom_components/badconga/camera.py index 77f0b69..072071f 100644 --- a/custom_components/badconga/camera.py +++ b/custom_components/badconga/camera.py @@ -23,6 +23,11 @@ def __init__(self, instance: Conga): self.instance.client.on('update_map', self.schedule_update_ha_state) self.instance.client.on('update_position', self.schedule_update_ha_state) + @property + def unique_id(self): + serial = self.instance.client.device.serial_number + return f"{serial}-camera" if serial else None + @property def name(self): return 'Conga' diff --git a/custom_components/badconga/manifest.json b/custom_components/badconga/manifest.json index 7e2f51d..b92daeb 100644 --- a/custom_components/badconga/manifest.json +++ b/custom_components/badconga/manifest.json @@ -2,7 +2,7 @@ "domain": "badconga", "name": "Bad Conga", "manifest_version": 2, - "version": "0.1.2", + "version": "0.1.3", "documentation": "https://github.com/adrigzr/badconga", "issue_tracker": "https://github.com/adrigzr/badconga/issues", "codeowners": [ diff --git a/custom_components/badconga/vacuum.py b/custom_components/badconga/vacuum.py index 949110c..3796e09 100644 --- a/custom_components/badconga/vacuum.py +++ b/custom_components/badconga/vacuum.py @@ -2,6 +2,7 @@ # pylint: disable=unused-argument import logging from functools import partial + from homeassistant.components.vacuum import ( VacuumEntity, SUPPORT_START, @@ -14,7 +15,17 @@ SUPPORT_LOCATE, SUPPORT_MAP ) -from .app.const import FAN_MODE_NONE, FAN_MODE_ECO, FAN_MODE_NORMAL, FAN_MODE_TURBO, MODEL_NAME + +from .app.const import ( + FAN_MODE_NONE, + FAN_MODE_ECO, + FAN_MODE_NORMAL, + FAN_MODE_TURBO, + MODEL_NAME, + STATUS_CODES, + BUSY_CODES +) + from .app.conga import Conga from . import DOMAIN @@ -42,6 +53,11 @@ def should_poll(self): def name(self): return 'Conga' + @property + def unique_id(self): + serial = self.instance.client.device.serial_number + return f'{serial}-vacuum' if serial else None + @property def state(self): return self.instance.client.device.state @@ -54,20 +70,32 @@ def status(self): @property def state_attributes(self): + ftype = self.instance.client.device.type + fcode = self.instance.client.device.fault_code + extended_status = ( + STATUS_CODES.get(fcode, fcode) if ftype and fcode else None + ) + + code = self.instance.client.device.busy_result + message = BUSY_CODES.get(code, code) if code else False + + size = self.instance.client.device.clean_size + clean_size = f'{ size / 100 } m²' if size else None + + time = self.instance.client.device.clean_time + clean_time = f'{ time } min' if size else None + + model = self.instance.client.device.model + model = MODEL_NAME.get(model, model) + data = super().state_attributes data['clean_mode'] = self.instance.client.device.clean_mode - data['robot_x'] = self.instance.client.map.robot.x - data['robot_y'] = self.instance.client.map.robot.y - data['robot_phi'] = self.instance.client.map.robot.phi - data['charger_x'] = self.instance.client.map.charger.x - data['charger_y'] = self.instance.client.map.charger.y - data['charger_phi'] = self.instance.client.map.charger.phi - data['clean_time'] = self.instance.client.device.clean_time - data['clean_size'] = self.instance.client.device.clean_size - data['name'] = self.instance.client.device.alias - data['model'] = MODEL_NAME[self.instance.client.device.model] if ( - self.instance.client.device.model in MODEL_NAME) else ( - self.instance.client.device.model) + data['extended_status'] = extended_status + data['message'] = message + data['clean_size'] = clean_size + data['clean_time'] = clean_time + data['alias'] = self.instance.client.device.alias + data['model'] = model data['serial'] = self.instance.client.device.serial_number data['revision'] = self.instance.client.device.controller_version data['firmware'] = self.instance.client.device.firmware_version