Skip to content

Commit

Permalink
Merge pull request #398 from greghesp/develop
Browse files Browse the repository at this point in the history
Merge Develop -> Main
  • Loading branch information
AdrianGarside authored Jan 6, 2024
2 parents b647e4f + d5ae17c commit 30bf704
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 82 deletions.
4 changes: 4 additions & 0 deletions blueprints/wled_controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ action:
entity_id: !input printer_stage
state: calibrating_extrusion
alias: Calibrating Extrusion
- condition: state
entity_id: !input printer_stage
state: calibrating_extrusion_flow
alias: Calibrating Extrusion Flow
- condition: state
entity_id: !input printer_status
state: offline
Expand Down
17 changes: 10 additions & 7 deletions custom_components/bambu_lab/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ async def async_step_Bambu_Lan(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
errors = {}
LOGGER.debug("async_step_Bambu_Choose_Device")
LOGGER.debug("async_step_Bambu_Lan")

device_list = await self.hass.async_add_executor_job(
self._bambu_cloud.get_device_list)
Expand Down Expand Up @@ -257,6 +257,9 @@ async def async_step_Lan(
errors = {}

if user_input is not None:
# Serial must be upper case to work
user_input['serial'] = user_input['serial'].upper()

LOGGER.debug("Config Flow: Testing local mqtt connection")
bambu = BambuClient(device_type="unknown",
serial=user_input['serial'],
Expand Down Expand Up @@ -297,8 +300,8 @@ async def async_step_Lan(

# Build form
fields: OrderedDict[vol.Marker, Any] = OrderedDict()
fields[vol.Required('serial', default = '' if user_input is None else user_input.get('serial', ''))] = TEXT_SELECTOR
fields[vol.Required('host', default = '' if user_input is None else user_input.get('host', ''))] = TEXT_SELECTOR
fields[vol.Required('serial', default = '' if user_input is None else user_input.get('serial', ''))] = TEXT_SELECTOR
fields[vol.Required('access_code', default = '' if user_input is None else user_input.get('access_code', ''))] = TEXT_SELECTOR

return self.async_show_form(
Expand Down Expand Up @@ -380,7 +383,7 @@ async def async_step_Bambu(
self.region = user_input['region']
self.email = user_input['email']

return await self.async_step_Bambu_Choose_Device(None)
return await self.async_step_Bambu_Lan(None)

except Exception as e:
LOGGER.error(f"Failed to connect with error code {e.args}")
Expand All @@ -389,7 +392,7 @@ async def async_step_Bambu(
elif credentialsGood:
self.region = self.config_entry.options['region']
self.email = self.config_entry.options['email']
return await self.async_step_Bambu_Choose_Device(None)
return await self.async_step_Bambu_Lan(None)

# Build form
fields: OrderedDict[vol.Marker, Any] = OrderedDict()
Expand All @@ -407,11 +410,11 @@ async def async_step_Bambu(
last_step=False,
)

async def async_step_Bambu_Choose_Device(
async def async_step_Bambu_Lan(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
errors = {}
LOGGER.debug("async_step_Bambu_Choose_Device")
LOGGER.debug("async_step_Bambu_Lan")

device_list = await self.hass.async_add_executor_job(
self._bambu_cloud.get_device_list)
Expand Down Expand Up @@ -484,7 +487,7 @@ async def async_step_Bambu_Choose_Device(
fields[vol.Optional('local_mqtt', default=self.config_entry.options.get('local_mqtt', True))] = BOOLEAN_SELECTOR

return self.async_show_form(
step_id="Bambu_Choose_Device",
step_id="Bambu_Lan",
data_schema=vol.Schema(fields),
errors=errors or {},
last_step=True,
Expand Down
6 changes: 6 additions & 0 deletions custom_components/bambu_lab/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ def event_handler(event):
case "event_print_started":
self.PublishDeviceTriggerEvent(event)

case "event_printer_chamber_image_update":
self._update_data()

case "event_printer_cover_image_update":
self._update_data()


async def listen():
self.client.connect(callback=event_handler)
Expand Down
14 changes: 7 additions & 7 deletions custom_components/bambu_lab/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
UnitOfTemperature,
UnitOfMass,
UnitOfLength,
TIME_MINUTES
UnitOfTime
)

from homeassistant.components.sensor import (
Expand All @@ -27,7 +27,7 @@
)

from .const import LOGGER
from .pybambu.const import SPEED_PROFILE, Features
from .pybambu.const import SPEED_PROFILE, Features, FansEnum


def fan_to_percent(speed):
Expand Down Expand Up @@ -161,15 +161,15 @@ class BambuLabBinarySensorEntityDescription(BinarySensorEntityDescription, Bambu
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:fan",
value_fn=lambda self: self.coordinator.get_model().fans.aux_fan_speed
value_fn=lambda self: self.coordinator.get_model().fans.get_fan_speed(FansEnum.AUXILIARY)
),
BambuLabSensorEntityDescription(
key="chamber_fan_speed",
translation_key="chamber_fan_speed",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:fan",
value_fn=lambda self: self.coordinator.get_model().fans.chamber_fan_speed,
value_fn=lambda self: self.coordinator.get_model().fans.get_fan_speed(FansEnum.CHAMBER),
exists_fn=lambda coordinator: coordinator.get_model().supports_feature(Features.CHAMBER_FAN)
),
BambuLabSensorEntityDescription(
Expand All @@ -178,15 +178,15 @@ class BambuLabBinarySensorEntityDescription(BinarySensorEntityDescription, Bambu
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:fan",
value_fn=lambda self: self.coordinator.get_model().fans.cooling_fan_speed
value_fn=lambda self: self.coordinator.get_model().fans.get_fan_speed(FansEnum.PART_COOLING)
),
BambuLabSensorEntityDescription(
key="heatbreak_fan_speed",
translation_key="heatbreak_fan_speed",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:fan",
value_fn=lambda self: self.coordinator.get_model().fans.heatbreak_fan_speed
value_fn=lambda self: self.coordinator.get_model().fans.get_fan_speed(FansEnum.HEATBREAK)
),
BambuLabSensorEntityDescription(
key="speed_profile",
Expand Down Expand Up @@ -261,7 +261,7 @@ class BambuLabBinarySensorEntityDescription(BinarySensorEntityDescription, Bambu
key="remaining_time",
translation_key="remaining_time",
icon="mdi:timer-sand",
native_unit_of_measurement=TIME_MINUTES,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
value_fn=lambda self: self.coordinator.get_model().info.remaining_time
),
Expand Down
12 changes: 6 additions & 6 deletions custom_components/bambu_lab/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ class BambuLabFanEntityDescription(FanEntityDescription, BambuLabFanEntityDescri
BambuLabFanEntityDescription(
key="cooling_fan",
translation_key="cooling_fan",
value_fn=lambda device: device.fans.cooling_fan_speed,
value_fn=lambda device: device.fans.get_fan_speed(FansEnum.PART_COOLING)
),
BambuLabFanEntityDescription(
key="aux_fan",
translation_key="aux_fan",
value_fn=lambda device: device.fans.aux_fan_speed
value_fn=lambda device: device.fans.get_fan_speed(FansEnum.AUXILIARY)
),
BambuLabFanEntityDescription(
key="chamber_fan",
translation_key="chamber_fan",
value_fn=lambda device: device.fans.chamber_fan_speed,
value_fn=lambda device: device.fans.get_fan_speed(FansEnum.CHAMBER),
exists_fn=lambda coordinator: coordinator.get_model().supports_feature(Features.CHAMBER_FAN)
)
)
Expand Down Expand Up @@ -100,11 +100,11 @@ def _set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
match self.entity_description.key:
case "cooling_fan":
self.coordinator.get_model().fans.SetFanSpeed(FansEnum.PART_COOLING, percentage)
self.coordinator.get_model().fans.set_fan_speed(FansEnum.PART_COOLING, percentage)
case "aux_fan":
self.coordinator.get_model().fans.SetFanSpeed(FansEnum.AUXILIARY, percentage)
self.coordinator.get_model().fans.set_fan_speed(FansEnum.AUXILIARY, percentage)
case "chamber_fan":
self.coordinator.get_model().fans.SetFanSpeed(FansEnum.CHAMBER, percentage)
self.coordinator.get_model().fans.set_fan_speed(FansEnum.CHAMBER, percentage)

def set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
Expand Down
2 changes: 1 addition & 1 deletion custom_components/bambu_lab/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
"st": "urn:bambulab-com:device:3dprinter:1"
}
],
"version": "2.0.4"
"version": "2.0.5"
}

85 changes: 63 additions & 22 deletions custom_components/bambu_lab/pybambu/bambu_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,46 +94,87 @@ def run(self):
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

jpeg_start = "ff d8 ff e0"
jpeg_end = "ff d9"

read_chunk_size = 1024

jpeg_start = bytearray([0xff, 0xd8, 0xff, 0xe0])
jpeg_end = bytearray([0xff, 0xd9])

read_chunk_size = 4096 # 4096 is the max we'll get even if we increase this.

# Payload format for each image is:
# 16 byte header:
# Bytes 0:3 = little endian payload size for the jpeg image (does not include this header).
# Bytes 4:7 = 0x00000000
# Bytes 8:11 = 0x00000001
# Bytes 12:15 = 0x00000000
# These first 16 bytes are always delivered by themselves.
#
# Bytes 16:19 = jpeg_start magic bytes
# Bytes 20:payload_size-2 = jpeg image bytes
# Bytes payload_size-2:payload_size = jpeg_end magic bytes
#
# Further attempts to receive data will get SSLWantReadError until a new image is ready (1-2 seconds later)
while not self._stop_event.is_set():
try:
with socket.create_connection((hostname, port)) as sock:
sslSock = ctx.wrap_socket(sock, server_hostname=hostname)
sslSock.write(d)
buf = bytearray()
start = False
img = None
payload_size = 0

sslSock.setblocking(False)
while not self._stop_event.is_set():
try:
dr = sslSock.recv(read_chunk_size)
#LOGGER.debug(f"{self._client._device.info.device_type}: Received {len(dr)} bytes.")

except ssl.SSLWantReadError:
#LOGGER.debug(f"{self._client._device.info.device_type}: SSLWantReadError")
time.sleep(1)
continue

except Exception as e:
LOGGER.error("A Chamber Image thread inner exception occurred:")
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
time.sleep(1) # Avoid a tight loop if this is a persistent error.
buf += dr

if not start:
i = buf.find(bytearray.fromhex(jpeg_start))
if i >= 0:
start = True
buf = buf[i:]
time.sleep(1)
continue

i = buf.find(bytearray.fromhex(jpeg_end))
if i >= 0:
img = buf[:i + len(jpeg_end)]
buf = buf[i + len(jpeg_end):]
start = False
if img is not None and len(dr) > 0:
img += dr
if len(img) > payload_size:
# We got more data than we expected.
LOGGER.error(f"Unexpected image payload received: {len(img)} > {payload_size}")
# Reset buffer
img = None
elif len(img) == payload_size:
# We should have the full image now.
if img[:4] != jpeg_start:
LOGGER.error("JPEG start magic bytes missing.")
elif img[-2:] != jpeg_end:
LOGGER.error("JPEG end magic bytes missing.")
else:
# Content is as expected. Send it.
self._client.on_jpeg_received(img)

# Reset buffer
img = None
# else:
# Otherwise we need to continue looping without reseting the buffer to receive the remaining data
# and without delaying.

elif len(dr) == 16:
# We got the header bytes. Get the expected payload size from it and create the image buffer bytearray.
img = bytearray()
payload_size = int.from_bytes(dr[0:3], byteorder='little')

elif len(dr) == 0:
# This occurs if the wrong access code was provided.
LOGGER.error("Chamber image connection rejected by the printer. Check provided access code and IP address.")
LOGGER.info("Chamber image thread exited.")
return

else:
LOGGER.error(f"{self._client._device.info.device_type}: UNEXPECTED DATA RECEIVED: {len(dr)}")
time.sleep(1)

self._client.on_jpeg_received(img)
except Exception as e:
LOGGER.error("A Chamber Image thread outer exception occurred:")
LOGGER.error(f"Exception. Type: {type(e)} Args: {e}")
Expand Down Expand Up @@ -271,7 +312,7 @@ def on_connect(self,
self._watchdog = WatchdogThread(self)
self._watchdog.start()

if self._device.supports_feature(Features.CAMERA_IMAGE) and self.host != "":
if self._device.supports_feature(Features.CAMERA_IMAGE):
LOGGER.debug("Starting Chamber Image thread")
self._camera = ChamberImageThread(self)
self._camera.start()
Expand Down
7 changes: 4 additions & 3 deletions custom_components/bambu_lab/pybambu/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class FansEnum(Enum):
PART_COOLING = 1,
AUXILIARY = 2,
CHAMBER = 3,
HEATBREAK = 4,


ACTION_IDS = {
Expand Down Expand Up @@ -177,7 +178,7 @@ class FansEnum(Enum):
"0C00_0300_0002_0004": "First layer inspection not supported for current print.",
"0C00_0300_0002_0005": "First layer inspection timeout.",
"0C00_0300_0003_0006": "Purged filaments may have piled up.",
"0C00_0300_0003_0007": "Possible first layer were defected.",
"0C00_0300_0003_0007": "Possible first layer defects.",
"0C00_0300_0003_0008": "Possible spaghetti defects were detected.",
"0C00_0300_0001_0009": "The first layer inspection module rebooted abnormally.",
"0C00_0300_0003_000B": "Inspecting first layer.",
Expand Down Expand Up @@ -214,7 +215,7 @@ class FansEnum(Enum):
"0700_5100_0003_0001": "AMS is disabled, please load filament from spool holder.",
"07FF_2000_0002_0001": "External filament has run out, please load a new filament.",
"07FF_2000_0002_0002": "External filament is missing, please load a new filament.",
"07FF_2000_0002_0004": "External filament is missing, please load a new filament.",
"07FF_2000_0002_0004": "Please pull out the filament on the spool holder from the extruder.",
}

# These errors cover those that are AMS and/or slot specific.
Expand All @@ -228,7 +229,7 @@ class FansEnum(Enum):
"0700_1000_0001_0001": "AMS1 slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
"0700_1000_0001_0003": "AMS1 slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
"0700_1000_0002_0002": "AMS1 slot 1 motor is overloaded. The filament may be tangled or stuck.",
"0700_2000_0002_0001": "AMS1 slot 1 filament has been ran out.",
"0700_2000_0002_0001": "AMS1 slot 1 filament has run out.",
"0700_2000_0002_0002": "AMS1 slot 1 is empty.",
"0700_2000_0002_0003": "AMS1 slot 1 filament may be broken in AMS.",
"0700_2000_0002_0004": "AMS1 slot 1 filament may be broken in the tool head.",
Expand Down
Loading

0 comments on commit 30bf704

Please sign in to comment.