Skip to content

Commit

Permalink
Merge pull request #18 from repier37/master
Browse files Browse the repository at this point in the history
  • Loading branch information
Lyr3x authored Jul 5, 2023
2 parents b09dfbe + 758a6bd commit f2d5f5f
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 28 deletions.
42 changes: 23 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
This is a work in progress. Entities are created properly and values can be read from the `rr_model` (standalone) or `/machine/status` (SBC) endpoint of your Duet board. The integration is meant to use with RRF 3.4.5 and onwards.
Ensure to select the correct mode (Standalone vs SBC)



## Installation

### From HACS
Expand All @@ -20,25 +22,27 @@ Ensure to select the correct mode (Standalone vs SBC)

### Config
Add the Duet3D Printer integration via the UI.
1. Parameters => Integrations
2. Add integration
3. Search Duet
4. Configure in UI the app
- Name => Name you want to give to your printer
- Host => Printer ip adress
- Port => Printer port => Usually 80
- Password => password, or empty if you don't have one , or if you are using SBC
- Update frequency
- Number of tools => Number of tools your printer has
- Hot bed => check if your printer has one
- LEDd's installed => check if your printer has LED
- Use standalone => check if your board is directly connected to your network. Uncheck if you are in SBC (duet board conencted to a rpi for example) see : [User manuel Duet](https://docs.duet3d.com/en/User_manual/Overview/Getting_started_Duet_3_MB6HC#:~:text=Standalone%20mode%20vs%20SBC%20mode%20The%20Duet%203,%28Duet%20Web%20Control%29%20etc%20work%20in%20both%20modes)

## Lovelace
A specific card exist for this integration:

[Duet integration card](https://github.com/repier37/ha-threedy-card)

![Featured](https://github.com/repier37/ha-threedy-card/raw/master/screenshots/active.png)

Add the following to your Lovelace dashboard. Remember to update the entity names with those of your own printer (defined by the value of `duet3d-name`)
```yaml
- card:
cards:
- type: glance
entities:
- entity: sensor.<name>_current_toolbed_temp
name: Bed
- entity: sensor.<name>_current_tool1_temp
name: Tool
- entity: sensor.<name>_current_state
name: Status
type: horizontal-stack
conditions:
- entity: switch.<name>
state: 'on'
type: conditional
```

There is also the possibility to send GCodes directly with a Home Assistant service:
```yaml
Expand All @@ -50,4 +54,4 @@ Currently is not working to log the responsen from an e.g `M122`


# Credits
Code initially based on the OctoPrint integration: [octoprint integration github](https://github.com/home-assistant/home-assistant/tree/dev/homeassistant/components/octoprint)
Code initially based on the OctoPrint integration: [octoprint integration github](https://github.com/home-assistant/home-assistant/tree/dev/homeassistant/components/octoprint)
33 changes: 28 additions & 5 deletions custom_components/duet3d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
CONF_PORT,
CONF_SSL,
Platform,
CONF_PASSWORD
)
from .const import (
CONF_NUMBER_OF_TOOLS,
Expand Down Expand Up @@ -155,13 +156,19 @@ def __init__(
self.status_error_logged = False
self.number_of_tools = self.config_entry.data[CONF_NUMBER_OF_TOOLS]
self.bed = self.config_entry.data[CONF_BED]
if self.config_entry.data[CONF_STANDALONE]:
self.status_api_url = "http{0}://{1}:{2}{3}".format(
self.base_url = "http{0}://{1}:{2}".format(
"s" if self.config_entry.data[CONF_SSL] else "",
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
CONF_STANDALONE_API,
)
config_entry.data[CONF_PORT]
)

if self.config_entry.data[CONF_STANDALONE]:
self.status_api_url = self.base_url+CONF_STANDALONE_API
#if standalone and has password
password = config_entry.data[CONF_PASSWORD]
if(len(password)>0):
self.identification_path = "/rr_connect?password=" + password
_LOGGER.warning("connection path: " + self.identification_path)
else:
self.status_api_url = "http{0}://{1}:{2}{3}{4}".format(
"s" if self.config_entry.data[CONF_SSL] else "",
Expand All @@ -174,6 +181,7 @@ def __init__(
self.board_model = (None,)
self.status_data = {}


def get_tools(self):
"""Get the list of tools that temperature is monitored on."""
tools = []
Expand All @@ -191,13 +199,28 @@ def get_tools(self):

async def get_status(self, key=None):
"""Send a get request, and return the response as a dict."""


# Only query the API at most every 30 seconds
if self.config_entry.data[CONF_STANDALONE]:
url = f"{self.status_api_url}?key={key}"
else:
url = self.status_api_url
_LOGGER.debug("URL: %s", url)

# send identification if required
try:
if (len(self.config_entry.data[CONF_PASSWORD])>0) :
connection_url = f"{self.base_url}{self.identification_path}"
async with async_timeout.timeout(10):
async with aiohttp.ClientSession() as session:
async with session.get(
connection_url, headers=CONF_JSON_HEADER
) as response:
response.raise_for_status()
except:
_LOGGER.error("Could not identify user to 3d printer")

try:
async with async_timeout.timeout(10):
async with aiohttp.ClientSession() as session:
Expand Down
11 changes: 7 additions & 4 deletions custom_components/duet3d/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from homeassistant.core import callback, HomeAssistant
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from typing import Any
from homeassistant.helpers.typing import UNDEFINED
Expand Down Expand Up @@ -38,6 +38,7 @@ def _schema_with_defaults(
ssl=False,
host="192.168.2.116",
port=80,
password="",
update_interval=30,
number_of_tools=1,
has_bed=True,
Expand All @@ -49,6 +50,7 @@ def _schema_with_defaults(
vol.Required(CONF_NAME, default=name): str,
vol.Required(CONF_SSL, default=ssl): bool,
vol.Required(CONF_HOST, default=host): str,
vol.Optional(CONF_PASSWORD, default=password): str,
vol.Required(CONF_PORT, default=port): cv.port,
vol.Required(CONF_INTERVAL, default=update_interval): int,
vol.Required(CONF_NUMBER_OF_TOOLS, default=number_of_tools): vol.Schema(
Expand All @@ -73,8 +75,8 @@ async def test_sbc_connection(base_url) -> str:
return response.status


async def test_standalone_connection(base_url) -> str:
connection_url = f"{base_url}/rr_connect?password=''"
async def test_standalone_connection(base_url, password) -> str:
connection_url = f"{base_url}/rr_connect?password={password}"
async with async_timeout.timeout(10):
async with aiohttp.ClientSession() as session:
async with session.get(
Expand Down Expand Up @@ -105,7 +107,7 @@ async def async_step_user(self, user_input=None):

try:
if user_input[CONF_STANDALONE]:
await test_standalone_connection(connection_url)
await test_standalone_connection(connection_url, user_input[CONF_PASSWORD])
else:
await test_sbc_connection(connection_url)
except (ClientError, asyncio.TimeoutError):
Expand All @@ -121,6 +123,7 @@ async def async_step_user(self, user_input=None):
CONF_NAME: user_input[CONF_NAME],
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_SSL: user_input[CONF_SSL],
CONF_INTERVAL: user_input[CONF_INTERVAL],
CONF_NUMBER_OF_TOOLS: user_input[CONF_NUMBER_OF_TOOLS],
Expand Down
3 changes: 3 additions & 0 deletions custom_components/duet3d/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
"icon": "mdi:axis-x-arrow",
},
"Thumbnail": {"json_path": "status.job.file.thumbnails", "icon": "mdi:picture"},
"Current Layer": {"json_path": "status.job.layer"},
"Total Layers": {"json_path": "status.job.file.numLayers"},
"File Name": {"json_path": "status.job.file.fileName"},
}

PRINTER_STATUS = {
Expand Down
101 changes: 101 additions & 0 deletions custom_components/duet3d/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Support for monitoring Duet3D sensors."""
import logging
import os
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand Down Expand Up @@ -102,6 +103,9 @@ def async_add_tool_sensors() -> None:
DuetPrintDurationSensor(coordinator, "Time Elapsed", device_id),
DuetPrintPositionSensor(coordinator, "Position (X,Y,Z)", device_id),
DuetCurrentStateSensor(coordinator, "Current State", device_id),
DuetCurrentLayerSensor(coordinator, "Current Layer", device_id),
DuetTotalLayersSensor(coordinator, "Total Layers", device_id),
DuetFileNameSensor(coordinator, "File Name", device_id),
]
async_add_entities(entities)

Expand Down Expand Up @@ -350,3 +354,100 @@ def native_value(self):
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success

class DuetCurrentLayerSensor(DuetPrintSensorBase):
"""Representation of an Duet3D sensor."""

_attr_icon = "mdi:layers"
_attr_state_class = SensorStateClass.MEASUREMENT

def __init__(
self,
coordinator: DuetDataUpdateCoordinator,
sensor_name: str,
device_id: str,
) -> None:
"""Initialize a new Duet3D sensor."""
super().__init__(
coordinator,
sensor_name,
f"{sensor_name}-{device_id}",
)

@property
def native_value(self):
"""Return sensor state."""
current_layer_json_path = SENSOR_TYPES["Current Layer"]["json_path"]
current_layer = self.coordinator.get_sensor_state(
current_layer_json_path, self.sensor_name
)
return current_layer

@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success

class DuetTotalLayersSensor(DuetPrintSensorBase):
"""Representation of an Duet3D sensor."""
_attr_icon = "mdi:layers-triple"
_attr_state_class = SensorStateClass.MEASUREMENT

def __init__(
self,
coordinator: DuetDataUpdateCoordinator,
sensor_name: str,
device_id: str,
) -> None:
"""Initialize a new Duet3D sensor."""
super().__init__(
coordinator,
sensor_name,
f"{sensor_name}-{device_id}",
)

@property
def native_value(self):
"""Return sensor state."""
total_layer_json_path = SENSOR_TYPES["Total Layers"]["json_path"]
total_layer = self.coordinator.get_sensor_state(
total_layer_json_path, self.sensor_name
)
return total_layer

@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success

class DuetFileNameSensor(DuetPrintSensorBase):
"""Representation of an Duet3D sensor."""
_attr_icon = "mdi:file"

def __init__(
self,
coordinator: DuetDataUpdateCoordinator,
sensor_name: str,
device_id: str,
) -> None:
"""Initialize a new Duet3D sensor."""
super().__init__(
coordinator,
sensor_name,
f"{sensor_name}-{device_id}",
)

@property
def native_value(self):
"""Return sensor state."""
file_name_json_path = SENSOR_TYPES["File Name"]["json_path"]
file_path = self.coordinator.get_sensor_state(
file_name_json_path, self.sensor_name
)
file_name = os.path.splitext(os.path.basename(file_path))[0]
return file_name

@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success
Binary file added images/active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f2d5f5f

Please sign in to comment.