diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 1874be6ec41..09c4b5c8ea7 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -2,86 +2,56 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_plm/ +https://home-assistant.io/components/binary_sensor.insteon_plm/ """ -import logging import asyncio +import logging -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.loader import get_component +from homeassistant.components.insteon_plm import InsteonPLMEntity DEPENDENCIES = ['insteon_plm'] _LOGGER = logging.getLogger(__name__) +SENSOR_TYPES = {'openClosedSensor': 'opening', + 'motionSensor': 'motion', + 'doorSensor': 'door', + 'leakSensor': 'moisture'} + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" plm = hass.data['insteon_plm'] - device_list = [] - for device in discovery_info: - name = device.get('address') - address = device.get('address_hex') + address = discovery_info['address'] + device = plm.devices[address] + state_key = discovery_info['state_key'] - _LOGGER.info('Registered %s with binary_sensor platform.', name) + _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + device.address.hex, device.states[state_key].name) - device_list.append( - InsteonPLMBinarySensorDevice(hass, plm, address, name) - ) + new_entity = InsteonPLMBinarySensor(device, state_key) - async_add_devices(device_list) + async_add_devices([new_entity]) -class InsteonPLMBinarySensorDevice(BinarySensorDevice): - """A Class for an Insteon device.""" +class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): + """A Class for an Insteon device entity.""" - def __init__(self, hass, plm, address, name): - """Initialize the binarysensor.""" - self._hass = hass - self._plm = plm.protocol - self._address = address - self._name = name - - self._plm.add_update_callback( - self.async_binarysensor_update, {'address': self._address}) + def __init__(self, device, state_key): + """Initialize the INSTEON PLM binary sensor.""" + super().__init__(device, state_key) + self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name) @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def address(self): - """Return the address of the node.""" - return self._address - - @property - def name(self): - """Return the name of the node.""" - return self._name + def device_class(self): + """Return the class of this sensor.""" + return self._sensor_type @property def is_on(self): """Return the boolean response if the node is on.""" - sensorstate = self._plm.get_device_attr(self._address, 'sensorstate') - _LOGGER.info("Sensor state for %s is %s", self._address, sensorstate) + sensorstate = self._insteon_device_state.value return bool(sensorstate) - - @property - def device_state_attributes(self): - """Provide attributes for display on device card.""" - insteon_plm = get_component('insteon_plm') - return insteon_plm.common_attributes(self) - - def get_attr(self, key): - """Return specified attribute for this device.""" - return self._plm.get_device_attr(self.address, key) - - @callback - def async_binarysensor_update(self, message): - """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update callback from PLM for %s", self._address) - self._hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/fan/insteon_plm.py b/homeassistant/components/fan/insteon_plm.py new file mode 100644 index 00000000000..f30abdbaa30 --- /dev/null +++ b/homeassistant/components/fan/insteon_plm.py @@ -0,0 +1,96 @@ +""" +Support for INSTEON fans via PowerLinc Modem. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/fan.insteon_plm/ +""" +import asyncio +import logging + +from homeassistant.components.fan import (SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + FanEntity, + SUPPORT_SET_SPEED) +from homeassistant.const import STATE_OFF +from homeassistant.components.insteon_plm import InsteonPLMEntity + +DEPENDENCIES = ['insteon_plm'] + +SPEED_TO_HEX = {SPEED_OFF: 0x00, + SPEED_LOW: 0x3f, + SPEED_MEDIUM: 0xbe, + SPEED_HIGH: 0xff} + +FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the INSTEON PLM device class for the hass platform.""" + plm = hass.data['insteon_plm'] + + address = discovery_info['address'] + device = plm.devices[address] + state_key = discovery_info['state_key'] + + _LOGGER.debug('Adding device %s entity %s to Fan platform', + device.address.hex, device.states[state_key].name) + + new_entity = InsteonPLMFan(device, state_key) + + async_add_devices([new_entity]) + + +class InsteonPLMFan(InsteonPLMEntity, FanEntity): + """An INSTEON fan component.""" + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._hex_to_speed(self._insteon_device_state.value) + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return FAN_SPEEDS + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @asyncio.coroutine + def async_turn_on(self, speed: str = None, **kwargs) -> None: + """Turn on the entity.""" + if speed is None: + speed = SPEED_MEDIUM + yield from self.async_set_speed(speed) + + @asyncio.coroutine + def async_turn_off(self, **kwargs) -> None: + """Turn off the entity.""" + yield from self.async_set_speed(SPEED_OFF) + + @asyncio.coroutine + def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + fan_speed = SPEED_TO_HEX[speed] + if fan_speed == 0x00: + self._insteon_device_state.off() + else: + self._insteon_device_state.set_level(fan_speed) + + @staticmethod + def _hex_to_speed(speed: int): + hex_speed = SPEED_OFF + if speed > 0xfe: + hex_speed = SPEED_HIGH + elif speed > 0x7f: + hex_speed = SPEED_MEDIUM + elif speed > 0: + hex_speed = SPEED_LOW + return hex_speed diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 4e2e8e02c7a..2381e3db69e 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -4,117 +4,211 @@ Support for INSTEON PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/insteon_plm/ """ -import logging import asyncio - +import collections +import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import ( - CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, + CONF_PLATFORM) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.7.5'] +REQUIREMENTS = ['insteonplm==0.8.2'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'insteon_plm' CONF_OVERRIDE = 'device_override' +CONF_ADDRESS = 'address' +CONF_CAT = 'cat' +CONF_SUBCAT = 'subcat' +CONF_FIRMWARE = 'firmware' +CONF_PRODUCT_KEY = 'product_key' + +CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( + cv.deprecated(CONF_PLATFORM), vol.Schema({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_CAT): cv.byte, + vol.Optional(CONF_SUBCAT): cv.byte, + vol.Optional(CONF_FIRMWARE): cv.byte, + vol.Optional(CONF_PRODUCT_KEY): cv.byte, + vol.Optional(CONF_PLATFORM): cv.string, + })) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PORT): cv.string, - vol.Optional(CONF_OVERRIDE, default=[]): cv.ensure_list_csv, - }) + vol.Optional(CONF_OVERRIDE): vol.All( + cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]) + }) }, extra=vol.ALLOW_EXTRA) -PLM_PLATFORMS = { - 'binary_sensor': ['binary_sensor'], - 'light': ['light'], - 'switch': ['switch'], -} - @asyncio.coroutine def async_setup(hass, config): """Set up the connection to the PLM.""" import insteonplm + ipdb = IPDB() + conf = config[DOMAIN] port = conf.get(CONF_PORT) - overrides = conf.get(CONF_OVERRIDE) + overrides = conf.get(CONF_OVERRIDE, []) @callback def async_plm_new_device(device): """Detect device from transport to be delegated to platform.""" - name = device.get('address') - address = device.get('address_hex') - capabilities = device.get('capabilities', []) + for state_key in device.states: + platform_info = ipdb[device.states[state_key]] + platform = platform_info.platform + if platform is not None: + _LOGGER.info("New INSTEON PLM device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform) - _LOGGER.info("New INSTEON PLM device: %s (%s) %r", - name, address, capabilities) - - loadlist = [] - for platform in PLM_PLATFORMS: - caplist = PLM_PLATFORMS.get(platform) - for key in capabilities: - if key in caplist: - loadlist.append(platform) - - loadlist = sorted(set(loadlist)) - - for loadplatform in loadlist: - hass.async_add_job( - discovery.async_load_platform( - hass, loadplatform, DOMAIN, discovered=[device], - hass_config=config)) + hass.async_add_job( + discovery.async_load_platform( + hass, platform, DOMAIN, + discovered={'address': device.address.hex, + 'state_key': state_key}, + hass_config=config)) _LOGGER.info("Looking for PLM on %s", port) - plm = yield from insteonplm.Connection.create(device=port, loop=hass.loop) + conn = yield from insteonplm.Connection.create( + device=port, + loop=hass.loop, + workdir=hass.config.config_dir) - for device in overrides: + plm = conn.protocol + + for device_override in overrides: # # Override the device default capabilities for a specific address # - if isinstance(device['platform'], list): - plm.protocol.devices.add_override( - device['address'], 'capabilities', device['platform']) - else: - plm.protocol.devices.add_override( - device['address'], 'capabilities', [device['platform']]) + address = device_override.get('address') + for prop in device_override: + if prop in [CONF_CAT, CONF_SUBCAT]: + plm.devices.add_override(address, prop, + device_override[prop]) + elif prop in [CONF_FIRMWARE, CONF_PRODUCT_KEY]: + plm.devices.add_override(address, CONF_PRODUCT_KEY, + device_override[prop]) hass.data['insteon_plm'] = plm - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, plm.close) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) - plm.protocol.devices.add_device_callback(async_plm_new_device, {}) + plm.devices.add_device_callback(async_plm_new_device) return True -def common_attributes(entity): - """Return the device state attributes.""" - attributes = {} - attributekeys = { - 'address': 'INSTEON Address', - 'description': 'Description', - 'model': 'Model', - 'cat': 'Category', - 'subcat': 'Subcategory', - 'firmware': 'Firmware', - 'product_key': 'Product Key' - } +State = collections.namedtuple('Product', 'stateType platform') - hexkeys = ['cat', 'subcat', 'firmware'] - for key in attributekeys: - name = attributekeys[key] - val = entity.get_attr(key) - if val is not None: - if key in hexkeys: - attributes[name] = hex(int(val)) - else: - attributes[name] = val - return attributes +class IPDB(object): + """Embodies the INSTEON Product Database static data and access methods.""" + + def __init__(self): + """Create the INSTEON Product Database (IPDB).""" + from insteonplm.states.onOff import (OnOffSwitch, + OnOffSwitch_OutletTop, + OnOffSwitch_OutletBottom, + OpenClosedRelay) + + from insteonplm.states.dimmable import (DimmableSwitch, + DimmableSwitch_Fan) + + from insteonplm.states.sensor import (VariableSensor, + OnOffSensor, + SmokeCO2Sensor, + IoLincSensor) + + self.states = [State(OnOffSwitch_OutletTop, 'switch'), + State(OnOffSwitch_OutletBottom, 'switch'), + State(OpenClosedRelay, 'switch'), + State(OnOffSwitch, 'switch'), + + State(IoLincSensor, 'binary_sensor'), + State(SmokeCO2Sensor, 'sensor'), + State(OnOffSensor, 'binary_sensor'), + State(VariableSensor, 'sensor'), + + State(DimmableSwitch_Fan, 'fan'), + State(DimmableSwitch, 'light')] + + def __len__(self): + """Return the number of INSTEON state types mapped to HA platforms.""" + return len(self.states) + + def __iter__(self): + """Itterate through the INSTEON state types to HA platforms.""" + for product in self.states: + yield product + + def __getitem__(self, key): + """Return a Home Assistant platform from an INSTEON state type.""" + for state in self.states: + if isinstance(key, state.stateType): + return state + return None + + +class InsteonPLMEntity(Entity): + """INSTEON abstract base entity.""" + + def __init__(self, device, state_key): + """Initialize the INSTEON PLM binary sensor.""" + self._insteon_device_state = device.states[state_key] + self._insteon_device = device + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def address(self): + """Return the address of the node.""" + return self._insteon_device.address.human + + @property + def group(self): + """Return the INSTEON group that the entity responds to.""" + return self._insteon_device_state.group + + @property + def name(self): + """Return the name of the node (used for Entity_ID).""" + name = '' + if self._insteon_device_state.group == 0x01: + name = self._insteon_device.id + else: + name = '{:s}_{:d}'.format(self._insteon_device.id, + self._insteon_device_state.group) + return name + + @property + def device_state_attributes(self): + """Provide attributes for display on device card.""" + attributes = { + 'INSTEON Address': self.address, + 'INSTEON Group': self.group + } + return attributes + + @callback + def async_entity_update(self, deviceid, statename, val): + """Receive notification from transport that new data exists.""" + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_added_to_hass(self): + """Register INSTEON update events.""" + self._insteon_device_state.register_updates( + self.async_entity_update) diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index f0ef0ce1b7e..40453da38e5 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -2,15 +2,14 @@ Support for Insteon lights via PowerLinc Modem. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_plm/ +https://home-assistant.io/components/light.insteon_plm/ """ -import logging import asyncio +import logging -from homeassistant.core import callback +from homeassistant.components.insteon_plm import InsteonPLMEntity from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -24,96 +23,47 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Insteon PLM device.""" plm = hass.data['insteon_plm'] - device_list = [] - for device in discovery_info: - name = device.get('address') - address = device.get('address_hex') - dimmable = bool('dimmable' in device.get('capabilities')) + address = discovery_info['address'] + device = plm.devices[address] + state_key = discovery_info['state_key'] - _LOGGER.info("Registered %s with light platform", name) + _LOGGER.debug('Adding device %s entity %s to Light platform', + device.address.hex, device.states[state_key].name) - device_list.append( - InsteonPLMDimmerDevice(hass, plm, address, name, dimmable) - ) + new_entity = InsteonPLMDimmerDevice(device, state_key) - async_add_devices(device_list) + async_add_devices([new_entity]) -class InsteonPLMDimmerDevice(Light): +class InsteonPLMDimmerDevice(InsteonPLMEntity, Light): """A Class for an Insteon device.""" - def __init__(self, hass, plm, address, name, dimmable): - """Initialize the light.""" - self._hass = hass - self._plm = plm.protocol - self._address = address - self._name = name - self._dimmable = dimmable - - self._plm.add_update_callback( - self.async_light_update, {'address': self._address}) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def address(self): - """Return the address of the node.""" - return self._address - - @property - def name(self): - """Return the name of the node.""" - return self._name - @property def brightness(self): """Return the brightness of this light between 0..255.""" - onlevel = self._plm.get_device_attr(self._address, 'onlevel') - _LOGGER.debug("on level for %s is %s", self._address, onlevel) + onlevel = self._insteon_device_state.value return int(onlevel) @property def is_on(self): """Return the boolean response if the node is on.""" - onlevel = self._plm.get_device_attr(self._address, 'onlevel') - _LOGGER.debug("on level for %s is %s", self._address, onlevel) - return bool(onlevel) + return bool(self.brightness) @property def supported_features(self): """Flag supported features.""" - if self._dimmable: - return SUPPORT_BRIGHTNESS - - @property - def device_state_attributes(self): - """Provide attributes for display on device card.""" - insteon_plm = get_component('insteon_plm') - return insteon_plm.common_attributes(self) - - def get_attr(self, key): - """Return specified attribute for this device.""" - return self._plm.get_device_attr(self.address, key) - - @callback - def async_light_update(self, message): - """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update callback from PLM for %s", self._address) - self._hass.async_add_job(self.async_update_ha_state()) + return SUPPORT_BRIGHTNESS @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn device on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) + self._insteon_device_state.set_level(brightness) else: - brightness = MAX_BRIGHTNESS - self._plm.turn_on(self._address, brightness=brightness) + self._insteon_device_state.on() @asyncio.coroutine def async_turn_off(self, **kwargs): """Turn device off.""" - self._plm.turn_off(self._address) + self._insteon_device_state.off() diff --git a/homeassistant/components/sensor/insteon_plm.py b/homeassistant/components/sensor/insteon_plm.py new file mode 100644 index 00000000000..a72b8efbc05 --- /dev/null +++ b/homeassistant/components/sensor/insteon_plm.py @@ -0,0 +1,36 @@ +""" +Support for INSTEON dimmers via PowerLinc Modem. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.insteon_plm/ +""" +import asyncio +import logging + +from homeassistant.components.insteon_plm import InsteonPLMEntity +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['insteon_plm'] + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the INSTEON PLM device class for the hass platform.""" + plm = hass.data['insteon_plm'] + + address = discovery_info['address'] + device = plm.devices[address] + state_key = discovery_info['state_key'] + + _LOGGER.debug('Adding device %s entity %s to Sensor platform', + device.address.hex, device.states[state_key].name) + + new_entity = InsteonPLMSensorDevice(device, state_key) + + async_add_devices([new_entity]) + + +class InsteonPLMSensorDevice(InsteonPLMEntity, Entity): + """A Class for an Insteon device.""" diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index 0b584e14b8d..5f9482ce955 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -2,14 +2,13 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon_plm/ +https://home-assistant.io/components/switch.insteon_plm/ """ -import logging import asyncio +import logging -from homeassistant.core import callback -from homeassistant.components.switch import (SwitchDevice) -from homeassistant.loader import get_component +from homeassistant.components.insteon_plm import InsteonPLMEntity +from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['insteon_plm'] @@ -21,77 +20,54 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" plm = hass.data['insteon_plm'] - device_list = [] - for device in discovery_info: - name = device.get('address') - address = device.get('address_hex') + address = discovery_info['address'] + device = plm.devices[address] + state_key = discovery_info['state_key'] - _LOGGER.info('Registered %s with switch platform.', name) + state_name = device.states[state_key].name - device_list.append( - InsteonPLMSwitchDevice(hass, plm, address, name) - ) + _LOGGER.debug('Adding device %s entity %s to Switch platform', + device.address.hex, device.states[state_key].name) - async_add_devices(device_list) + new_entity = None + if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff']: + new_entity = InsteonPLMSwitchDevice(device, state_key) + elif state_name == 'openClosedRelay': + new_entity = InsteonPLMOpenClosedDevice(device, state_key) + + if new_entity is not None: + async_add_devices([new_entity]) -class InsteonPLMSwitchDevice(SwitchDevice): +class InsteonPLMSwitchDevice(InsteonPLMEntity, SwitchDevice): """A Class for an Insteon device.""" - def __init__(self, hass, plm, address, name): - """Initialize the switch.""" - self._hass = hass - self._plm = plm.protocol - self._address = address - self._name = name - - self._plm.add_update_callback( - self.async_switch_update, {'address': self._address}) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def address(self): - """Return the address of the node.""" - return self._address - - @property - def name(self): - """Return the name of the node.""" - return self._name - @property def is_on(self): """Return the boolean response if the node is on.""" - onlevel = self._plm.get_device_attr(self._address, 'onlevel') - _LOGGER.debug('on level for %s is %s', self._address, onlevel) + onlevel = self._insteon_device_state.value return bool(onlevel) - @property - def device_state_attributes(self): - """Provide attributes for display on device card.""" - insteon_plm = get_component('insteon_plm') - return insteon_plm.common_attributes(self) - - def get_attr(self, key): - """Return specified attribute for this device.""" - return self._plm.get_device_attr(self.address, key) - - @callback - def async_switch_update(self, message): - """Receive notification from transport that new data exists.""" - _LOGGER.info('Received update callback from PLM for %s', self._address) - self._hass.async_add_job(self.async_update_ha_state()) - @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn device on.""" - self._plm.turn_on(self._address) + self._insteon_device_state.on() @asyncio.coroutine def async_turn_off(self, **kwargs): """Turn device off.""" - self._plm.turn_off(self._address) + self._insteon_device_state.off() + + +class InsteonPLMOpenClosedDevice(InsteonPLMEntity, SwitchDevice): + """A Class for an Insteon device.""" + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn device on.""" + self._insteon_device_state.open() + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn device off.""" + self._insteon_device_state.close() diff --git a/requirements_all.txt b/requirements_all.txt index ce639243a98..fa55aa20233 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.7.5 +insteonplm==0.8.2 # homeassistant.components.verisure jsonpath==0.75