diff --git a/.coveragerc b/.coveragerc index 9f6bb0d1b95..030c48cd10c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -488,6 +488,8 @@ omit = homeassistant/components/reddit/* homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py + homeassistant/components/repetier/__init__.py + homeassistant/components/repetier/sensor.py homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/binary_sensor.py homeassistant/components/rest/notify.py diff --git a/CODEOWNERS b/CODEOWNERS index 1e7c3c87a07..59bd8c31af1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -193,6 +193,7 @@ homeassistant/components/qwikswitch/* @kellerza homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py new file mode 100755 index 00000000000..24382b2f12d --- /dev/null +++ b/homeassistant/components/repetier/__init__.py @@ -0,0 +1,248 @@ +"""Support for Repetier-Server sensors.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PORT, + CONF_SENSORS, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval +from homeassistant.util import slugify as util_slugify + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'RepetierServer' +DOMAIN = 'repetier' +REPETIER_API = 'repetier_api' +SCAN_INTERVAL = timedelta(seconds=10) +UPDATE_SIGNAL = 'repetier_update_signal' + +TEMP_DATA = { + 'tempset': 'temp_set', + 'tempread': 'state', + 'output': 'output', +} + + +API_PRINTER_METHODS = { + 'bed_temperature': { + 'offline': {'heatedbeds': None, 'state': 'off'}, + 'state': {'heatedbeds': 'temp_data'}, + 'temp_data': TEMP_DATA, + 'attribute': 'heatedbeds', + }, + 'extruder_temperature': { + 'offline': {'extruder': None, 'state': 'off'}, + 'state': {'extruder': 'temp_data'}, + 'temp_data': TEMP_DATA, + 'attribute': 'extruder', + }, + 'chamber_temperature': { + 'offline': {'heatedchambers': None, 'state': 'off'}, + 'state': {'heatedchambers': 'temp_data'}, + 'temp_data': TEMP_DATA, + 'attribute': 'heatedchambers', + }, + 'current_state': { + 'offline': {'state': None}, + 'state': { + 'state': 'state', + 'activeextruder': 'active_extruder', + 'hasxhome': 'x_homed', + 'hasyhome': 'y_homed', + 'haszhome': 'z_homed', + 'firmware': 'firmware', + 'firmwareurl': 'firmware_url', + }, + }, + 'current_job': { + 'offline': {'job': None, 'state': 'off'}, + 'state': { + 'done': 'state', + 'job': 'job_name', + 'jobid': 'job_id', + 'totallines': 'total_lines', + 'linessent': 'lines_sent', + 'oflayer': 'total_layers', + 'layer': 'current_layer', + 'speedmultiply': 'feed_rate', + 'flowmultiply': 'flow', + 'x': 'x', + 'y': 'y', + 'z': 'z', + }, + }, + 'job_end': { + 'offline': { + 'job': None, 'state': 'off', 'start': None, 'printtime': None}, + 'state': { + 'job': 'job_name', + 'start': 'start', + 'printtime': 'print_time', + 'printedtimecomp': 'from_start', + }, + }, + 'job_start': { + 'offline': { + 'job': None, + 'state': 'off', + 'start': None, + 'printedtimecomp': None + }, + 'state': { + 'job': 'job_name', + 'start': 'start', + 'printedtimecomp': 'from_start', + }, + }, +} + + +def has_all_unique_names(value): + """Validate that printers have an unique name.""" + names = [util_slugify(printer[CONF_NAME]) for printer in value] + vol.Schema(vol.Unique())(names) + return value + + +SENSOR_TYPES = { + # Type, Unit, Icon + 'bed_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer', + '_bed_'], + 'extruder_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer', + '_extruder_'], + 'chamber_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer', + '_chamber_'], + 'current_state': ['state', None, 'mdi:printer-3d', ''], + 'current_job': ['progress', '%', 'mdi:file-percent', '_current_job'], + 'job_end': ['progress', None, 'mdi:clock-end', '_job_end'], + 'job_start': ['progress', None, 'mdi:clock-start', '_job_start'], +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=3344): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + })], has_all_unique_names), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Repetier Server component.""" + import pyrepetier + + hass.data[REPETIER_API] = {} + + for repetier in config[DOMAIN]: + _LOGGER.debug("Repetier server config %s", repetier[CONF_HOST]) + + url = "http://{}".format(repetier[CONF_HOST]) + port = repetier[CONF_PORT] + api_key = repetier[CONF_API_KEY] + + client = pyrepetier.Repetier( + url=url, + port=port, + apikey=api_key) + + printers = client.getprinters() + + if not printers: + return False + + sensors = repetier[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + api = PrinterAPI(hass, client, printers, sensors, + repetier[CONF_NAME], config) + api.update() + track_time_interval(hass, api.update, SCAN_INTERVAL) + + hass.data[REPETIER_API][repetier[CONF_NAME]] = api + + return True + + +class PrinterAPI: + """Handle the printer API.""" + + def __init__(self, hass, client, printers, sensors, conf_name, config): + """Set up instance.""" + self._hass = hass + self._client = client + self.printers = printers + self.sensors = sensors + self.conf_name = conf_name + self.config = config + self._known_entities = set() + + def get_data(self, printer_id, sensor_type, temp_id): + """Get data from the state cache.""" + printer = self.printers[printer_id] + methods = API_PRINTER_METHODS[sensor_type] + for prop, offline in methods['offline'].items(): + state = getattr(printer, prop) + if state == offline: + # if state matches offline, sensor is offline + return None + + data = {} + for prop, attr in methods['state'].items(): + prop_data = getattr(printer, prop) + if attr == 'temp_data': + temp_methods = methods['temp_data'] + for temp_prop, temp_attr in temp_methods.items(): + data[temp_attr] = getattr(prop_data[temp_id], temp_prop) + else: + data[attr] = prop_data + return data + + def update(self, now=None): + """Update the state cache from the printer API.""" + for printer in self.printers: + printer.get_data() + self._load_entities() + dispatcher_send(self._hass, UPDATE_SIGNAL) + + def _load_entities(self): + sensor_info = [] + for pidx, printer in enumerate(self.printers): + for sensor_type in self.sensors: + info = {} + info['sensor_type'] = sensor_type + info['printer_id'] = pidx + info['name'] = printer.slug + info['printer_name'] = self.conf_name + + known = '{}-{}'.format(printer.slug, sensor_type) + if known in self._known_entities: + continue + + methods = API_PRINTER_METHODS[sensor_type] + if 'temp_data' in methods['state'].values(): + prop_data = getattr(printer, methods['attribute']) + if prop_data is None: + continue + for idx, _ in enumerate(prop_data): + info['temp_id'] = idx + sensor_info.append(info) + else: + info['temp_id'] = None + sensor_info.append(info) + + self._known_entities.add(known) + + if not sensor_info: + return + load_platform(self._hass, 'sensor', DOMAIN, sensor_info, self.config) diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json new file mode 100755 index 00000000000..14af98cfb64 --- /dev/null +++ b/homeassistant/components/repetier/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "repetier", + "name": "Repetier Server", + "documentation": "https://www.home-assistant.io/components/repetier", + "requirements": [ + "pyrepetier==3.0.5" + ], + "dependencies": [], + "codeowners": ["@MTrab"] +} diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py new file mode 100755 index 00000000000..17f999a95cf --- /dev/null +++ b/homeassistant/components/repetier/sensor.py @@ -0,0 +1,215 @@ +"""Support for monitoring Repetier Server Sensors.""" +from datetime import datetime +import logging +import time + +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available Repetier Server sensors.""" + if discovery_info is None: + return + + sensor_map = { + 'bed_temperature': RepetierTempSensor, + 'extruder_temperature': RepetierTempSensor, + 'chamber_temperature': RepetierTempSensor, + 'current_state': RepetierSensor, + 'current_job': RepetierJobSensor, + 'job_end': RepetierJobEndSensor, + 'job_start': RepetierJobStartSensor, + } + + entities = [] + for info in discovery_info: + printer_name = info['printer_name'] + api = hass.data[REPETIER_API][printer_name] + printer_id = info['printer_id'] + sensor_type = info['sensor_type'] + temp_id = info['temp_id'] + name = info['name'] + if temp_id is not None: + name = '{}{}{}'.format( + name, SENSOR_TYPES[sensor_type][3], temp_id) + else: + name = '{}{}'.format(name, SENSOR_TYPES[sensor_type][3]) + sensor_class = sensor_map[sensor_type] + entity = sensor_class(api, temp_id, name, printer_id, sensor_type) + entities.append(entity) + + add_entities(entities, True) + + +class RepetierSensor(Entity): + """Class to create and populate a Repetier Sensor.""" + + def __init__(self, api, temp_id, name, printer_id, sensor_type): + """Init new sensor.""" + self._api = api + self._attributes = {} + self._available = False + self._temp_id = temp_id + self._name = name + self._printer_id = printer_id + self._sensor_type = sensor_type + self._state = None + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return sensor attributes.""" + return self._attributes + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self._sensor_type][1] + + @property + def icon(self): + """Icon to use in the frontend.""" + return SENSOR_TYPES[self._sensor_type][2] + + @property + def should_poll(self): + """Return False as entity is updated from the component.""" + return False + + @property + def state(self): + """Return sensor state.""" + return self._state + + @callback + def update_callback(self): + """Get new data and update state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Connect update callbacks.""" + async_dispatcher_connect( + self.hass, UPDATE_SIGNAL, self.update_callback) + + def _get_data(self): + """Return new data from the api cache.""" + data = self._api.get_data( + self._printer_id, self._sensor_type, self._temp_id) + if data is None: + _LOGGER.debug( + "Data not found for %s and %s", + self._sensor_type, self._temp_id) + self._available = False + return None + self._available = True + return data + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + state = data.pop('state') + _LOGGER.debug("Printer %s State %s", self._name, state) + self._attributes.update(data) + self._state = state + + +class RepetierTempSensor(RepetierSensor): + """Represent a Repetier temp sensor.""" + + @property + def state(self): + """Return sensor state.""" + if self._state is None: + return None + return round(self._state, 2) + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + state = data.pop('state') + temp_set = data['temp_set'] + _LOGGER.debug( + "Printer %s Setpoint: %s, Temp: %s", + self._name, temp_set, state) + self._attributes.update(data) + self._state = state + + +class RepetierJobSensor(RepetierSensor): + """Represent a Repetier job sensor.""" + + @property + def state(self): + """Return sensor state.""" + if self._state is None: + return None + return round(self._state, 2) + + +class RepetierJobEndSensor(RepetierSensor): + """Class to create and populate a Repetier Job End timestamp Sensor.""" + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + job_name = data['job_name'] + start = data['start'] + print_time = data['print_time'] + from_start = data['from_start'] + time_end = start + round(print_time, 0) + self._state = datetime.utcfromtimestamp(time_end).isoformat() + remaining = print_time - from_start + remaining_secs = int(round(remaining, 0)) + _LOGGER.debug( + "Job %s remaining %s", + job_name, time.strftime('%H:%M:%S', time.gmtime(remaining_secs))) + + +class RepetierJobStartSensor(RepetierSensor): + """Class to create and populate a Repetier Job Start timestamp Sensor.""" + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + job_name = data['job_name'] + start = data['start'] + from_start = data['from_start'] + self._state = datetime.utcfromtimestamp(start).isoformat() + elapsed_secs = int(round(from_start, 0)) + _LOGGER.debug( + "Job %s elapsed %s", + job_name, time.strftime('%H:%M:%S', time.gmtime(elapsed_secs))) diff --git a/requirements_all.txt b/requirements_all.txt index f6367e30758..f045a546798 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1290,6 +1290,9 @@ pyrainbird==0.1.6 # homeassistant.components.recswitch pyrecswitch==1.0.2 +# homeassistant.components.repetier +pyrepetier==3.0.5 + # homeassistant.components.ruter pyruter==1.1.0