Guest User

Untitled

a guest
Dec 3rd, 2020
98
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 19.89 KB | None | 0 0
  1. """Support for Xiaomi Smart WiFi Socket and Smart Power Strip."""
  2. import asyncio
  3. from functools import partial
  4. import logging
  5.  
  6. from miio import (  # pylint: disable=import-error
  7.     AirConditioningCompanionV3,
  8.     ChuangmiPlug,
  9.     Device,
  10.     DeviceException,
  11.     PowerStrip,
  12.     CurtainMiot,
  13. )
  14. from miio.powerstrip import PowerMode  # pylint: disable=import-error
  15. from miio.curtain_youpin import MotorControl
  16. import voluptuous as vol
  17.  
  18. from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
  19. from homeassistant.const import (
  20.     ATTR_ENTITY_ID,
  21.     ATTR_MODE,
  22.     CONF_HOST,
  23.     CONF_NAME,
  24.     CONF_TOKEN,
  25. )
  26. from homeassistant.exceptions import PlatformNotReady
  27. import homeassistant.helpers.config_validation as cv
  28.  
  29. from .const import (
  30.     DOMAIN,
  31.     SERVICE_SET_POWER_MODE,
  32.     SERVICE_SET_POWER_PRICE,
  33.     SERVICE_SET_WIFI_LED_OFF,
  34.     SERVICE_SET_WIFI_LED_ON,
  35. )
  36.  
  37. _LOGGER = logging.getLogger(__name__)
  38.  
  39. DEFAULT_NAME = "Xiaomi Miio Switch"
  40. DATA_KEY = "switch.xiaomi_miio"
  41.  
  42. CONF_MODEL = "model"
  43. MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2"
  44. MODEL_PLUG_V3 = "chuangmi.plug.v3"
  45.  
  46. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
  47.     {
  48.         vol.Required(CONF_HOST): cv.string,
  49.         vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
  50.         vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
  51.         vol.Optional(CONF_MODEL): vol.In(
  52.             [
  53.                 "chuangmi.plug.v1",
  54.                 "qmi.powerstrip.v1",
  55.                 "zimi.powerstrip.v2",
  56.                 "chuangmi.plug.m1",
  57.                 "chuangmi.plug.m3",
  58.                 "chuangmi.plug.v2",
  59.                 "chuangmi.plug.v3",
  60.                 "chuangmi.plug.hmi205",
  61.                 "chuangmi.plug.hmi206",
  62.                 "chuangmi.plug.hmi208",
  63.                 "lumi.acpartner.v3",
  64.                 "lumi.curtain.hagl05",
  65.             ]
  66.         ),
  67.     }
  68. )
  69.  
  70. ATTR_POWER = "power"
  71. ATTR_TEMPERATURE = "temperature"
  72. ATTR_LOAD_POWER = "load_power"
  73. ATTR_MODEL = "model"
  74. ATTR_POWER_MODE = "power_mode"
  75. ATTR_WIFI_LED = "wifi_led"
  76. ATTR_POWER_PRICE = "power_price"
  77. ATTR_PRICE = "price"
  78.  
  79. SUCCESS = ["ok"]
  80.  
  81. FEATURE_SET_POWER_MODE = 1
  82. FEATURE_SET_WIFI_LED = 2
  83. FEATURE_SET_POWER_PRICE = 4
  84.  
  85. FEATURE_FLAGS_GENERIC = 0
  86.  
  87. FEATURE_FLAGS_POWER_STRIP_V1 = (
  88.     FEATURE_SET_POWER_MODE | FEATURE_SET_WIFI_LED | FEATURE_SET_POWER_PRICE
  89. )
  90.  
  91. FEATURE_FLAGS_POWER_STRIP_V2 = FEATURE_SET_WIFI_LED | FEATURE_SET_POWER_PRICE
  92.  
  93. FEATURE_FLAGS_PLUG_V3 = FEATURE_SET_WIFI_LED
  94.  
  95. SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
  96.  
  97. SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend(
  98.     {vol.Required(ATTR_MODE): vol.All(vol.In(["green", "normal"]))}
  99. )
  100.  
  101. SERVICE_SCHEMA_POWER_PRICE = SERVICE_SCHEMA.extend(
  102.     {vol.Required(ATTR_PRICE): cv.positive_float}
  103. )
  104.  
  105. SERVICE_TO_METHOD = {
  106.     SERVICE_SET_WIFI_LED_ON: {"method": "async_set_wifi_led_on"},
  107.     SERVICE_SET_WIFI_LED_OFF: {"method": "async_set_wifi_led_off"},
  108.     SERVICE_SET_POWER_MODE: {
  109.         "method": "async_set_power_mode",
  110.         "schema": SERVICE_SCHEMA_POWER_MODE,
  111.     },
  112.     SERVICE_SET_POWER_PRICE: {
  113.         "method": "async_set_power_price",
  114.         "schema": SERVICE_SCHEMA_POWER_PRICE,
  115.     },
  116. }
  117.  
  118.  
  119. async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
  120.     """Set up the switch from config."""
  121.     if DATA_KEY not in hass.data:
  122.         hass.data[DATA_KEY] = {}
  123.  
  124.     host = config[CONF_HOST]
  125.     token = config[CONF_TOKEN]
  126.     name = config[CONF_NAME]
  127.     model = config.get(CONF_MODEL)
  128.  
  129.     _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
  130.  
  131.     devices = []
  132.     unique_id = None
  133.  
  134.     if model is None:
  135.         try:
  136.             miio_device = Device(host, token)
  137.             device_info = await hass.async_add_executor_job(miio_device.info)
  138.             model = device_info.model
  139.             unique_id = f"{model}-{device_info.mac_address}"
  140.             _LOGGER.info(
  141.                 "%s %s %s detected",
  142.                 model,
  143.                 device_info.firmware_version,
  144.                 device_info.hardware_version,
  145.             )
  146.         except DeviceException as ex:
  147.             raise PlatformNotReady from ex
  148.  
  149.     if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
  150.         plug = ChuangmiPlug(host, token, model=model)
  151.  
  152.         # The device has two switchable channels (mains and a USB port).
  153.         # A switch device per channel will be created.
  154.         for channel_usb in [True, False]:
  155.             device = ChuangMiPlugSwitch(name, plug, model, unique_id, channel_usb)
  156.             devices.append(device)
  157.             hass.data[DATA_KEY][host] = device
  158.  
  159.     elif model in ['lumi.curtain.hagl05']:
  160.         plug = CurtainMiot(host, token)
  161.         device = XiaomiCurtainMotor(name, plug, model, unique_id)
  162.         devices.append(device)
  163.         hass.data[DATA_KEY][host] = device
  164.     elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]:
  165.         plug = PowerStrip(host, token, model=model)
  166.         device = XiaomiPowerStripSwitch(name, plug, model, unique_id)
  167.         devices.append(device)
  168.         hass.data[DATA_KEY][host] = device
  169.     elif model in [
  170.         "chuangmi.plug.m1",
  171.         "chuangmi.plug.m3",
  172.         "chuangmi.plug.v2",
  173.         "chuangmi.plug.hmi205",
  174.         "chuangmi.plug.hmi206",
  175.     ]:
  176.         plug = ChuangmiPlug(host, token, model=model)
  177.         device = XiaomiPlugGenericSwitch(name, plug, model, unique_id)
  178.         devices.append(device)
  179.         hass.data[DATA_KEY][host] = device
  180.     elif model in ["lumi.acpartner.v3"]:
  181.         plug = AirConditioningCompanionV3(host, token)
  182.         device = XiaomiAirConditioningCompanionSwitch(name, plug, model, unique_id)
  183.         devices.append(device)
  184.         hass.data[DATA_KEY][host] = device
  185.     else:
  186.         _LOGGER.error(
  187.             "Unsupported device found! Please create an issue at "
  188.             "https://github.com/rytilahti/python-miio/issues "
  189.             "and provide the following data: %s",
  190.             model,
  191.         )
  192.         return False
  193.  
  194.     async_add_entities(devices, update_before_add=True)
  195.  
  196.     async def async_service_handler(service):
  197.         """Map services to methods on XiaomiPlugGenericSwitch."""
  198.         method = SERVICE_TO_METHOD.get(service.service)
  199.         params = {
  200.             key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
  201.         }
  202.         entity_ids = service.data.get(ATTR_ENTITY_ID)
  203.         if entity_ids:
  204.             devices = [
  205.                 device
  206.                 for device in hass.data[DATA_KEY].values()
  207.                 if device.entity_id in entity_ids
  208.             ]
  209.         else:
  210.             devices = hass.data[DATA_KEY].values()
  211.  
  212.         update_tasks = []
  213.         for device in devices:
  214.             if not hasattr(device, method["method"]):
  215.                 continue
  216.             await getattr(device, method["method"])(**params)
  217.             update_tasks.append(device.async_update_ha_state(True))
  218.  
  219.         if update_tasks:
  220.             await asyncio.wait(update_tasks)
  221.  
  222.     for plug_service in SERVICE_TO_METHOD:
  223.         schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA)
  224.         hass.services.async_register(
  225.             DOMAIN, plug_service, async_service_handler, schema=schema
  226.         )
  227.  
  228.  
  229. class XiaomiPlugGenericSwitch(SwitchEntity):
  230.     """Representation of a Xiaomi Plug Generic."""
  231.  
  232.     def __init__(self, name, plug, model, unique_id):
  233.         """Initialize the plug switch."""
  234.         self._name = name
  235.         self._plug = plug
  236.         self._model = model
  237.         self._unique_id = unique_id
  238.  
  239.         self._icon = "mdi:power-socket"
  240.         self._available = False
  241.         self._state = None
  242.         self._state_attrs = {ATTR_TEMPERATURE: None, ATTR_MODEL: self._model}
  243.         self._device_features = FEATURE_FLAGS_GENERIC
  244.         self._skip_update = False
  245.  
  246.     @property
  247.     def unique_id(self):
  248.         """Return an unique ID."""
  249.         return self._unique_id
  250.  
  251.     @property
  252.     def name(self):
  253.         """Return the name of the device if any."""
  254.         return self._name
  255.  
  256.     @property
  257.     def icon(self):
  258.         """Return the icon to use for device if any."""
  259.         return self._icon
  260.  
  261.     @property
  262.     def available(self):
  263.         """Return true when state is known."""
  264.         return self._available
  265.  
  266.     @property
  267.     def device_state_attributes(self):
  268.         """Return the state attributes of the device."""
  269.         return self._state_attrs
  270.  
  271.     @property
  272.     def is_on(self):
  273.         """Return true if switch is on."""
  274.         return self._state
  275.  
  276.     async def _try_command(self, mask_error, func, *args, **kwargs):
  277.         """Call a plug command handling error messages."""
  278.         try:
  279.             result = await self.hass.async_add_executor_job(
  280.                 partial(func, *args, **kwargs)
  281.             )
  282.  
  283.             _LOGGER.debug("Response received from plug: %s", result)
  284.  
  285.             # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off.
  286.             if func in ["usb_on", "usb_off"] and result == 0:
  287.                 return True
  288.  
  289.             return result == SUCCESS
  290.         except DeviceException as exc:
  291.             if self._available:
  292.                 _LOGGER.error(mask_error, exc)
  293.                 self._available = False
  294.  
  295.             return False
  296.  
  297.     async def async_turn_on(self, **kwargs):
  298.         """Turn the plug on."""
  299.         result = await self._try_command("Turning the plug on failed.", self._plug.on)
  300.  
  301.         if result:
  302.             self._state = True
  303.             self._skip_update = True
  304.  
  305.     async def async_turn_off(self, **kwargs):
  306.         """Turn the plug off."""
  307.         result = await self._try_command("Turning the plug off failed.", self._plug.off)
  308.  
  309.         if result:
  310.             self._state = False
  311.             self._skip_update = True
  312.  
  313.     async def async_update(self):
  314.         """Fetch state from the device."""
  315.         # On state change the device doesn't provide the new state immediately.
  316.         if self._skip_update:
  317.             self._skip_update = False
  318.             return
  319.  
  320.         try:
  321.             state = await self.hass.async_add_executor_job(self._plug.status)
  322.             _LOGGER.debug("Got new state: %s", state)
  323.  
  324.             self._available = True
  325.             self._state = state.is_on
  326.             self._state_attrs[ATTR_TEMPERATURE] = state.temperature
  327.  
  328.         except DeviceException as ex:
  329.             if self._available:
  330.                 self._available = False
  331.                 _LOGGER.error("Got exception while fetching the state: %s", ex)
  332.  
  333.     async def async_set_wifi_led_on(self):
  334.         """Turn the wifi led on."""
  335.         if self._device_features & FEATURE_SET_WIFI_LED == 0:
  336.             return
  337.  
  338.         await self._try_command(
  339.             "Turning the wifi led on failed.", self._plug.set_wifi_led, True
  340.         )
  341.  
  342.     async def async_set_wifi_led_off(self):
  343.         """Turn the wifi led on."""
  344.         if self._device_features & FEATURE_SET_WIFI_LED == 0:
  345.             return
  346.  
  347.         await self._try_command(
  348.             "Turning the wifi led off failed.", self._plug.set_wifi_led, False
  349.         )
  350.  
  351.     async def async_set_power_price(self, price: int):
  352.         """Set the power price."""
  353.         if self._device_features & FEATURE_SET_POWER_PRICE == 0:
  354.             return
  355.  
  356.         await self._try_command(
  357.             "Setting the power price of the power strip failed.",
  358.             self._plug.set_power_price,
  359.             price,
  360.         )
  361.  
  362.  
  363. class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
  364.     """Representation of a Xiaomi Power Strip."""
  365.  
  366.     def __init__(self, name, plug, model, unique_id):
  367.         """Initialize the plug switch."""
  368.         super().__init__(name, plug, model, unique_id)
  369.  
  370.         if self._model == MODEL_POWER_STRIP_V2:
  371.             self._device_features = FEATURE_FLAGS_POWER_STRIP_V2
  372.         else:
  373.             self._device_features = FEATURE_FLAGS_POWER_STRIP_V1
  374.  
  375.         self._state_attrs[ATTR_LOAD_POWER] = None
  376.  
  377.         if self._device_features & FEATURE_SET_POWER_MODE == 1:
  378.             self._state_attrs[ATTR_POWER_MODE] = None
  379.  
  380.         if self._device_features & FEATURE_SET_WIFI_LED == 1:
  381.             self._state_attrs[ATTR_WIFI_LED] = None
  382.  
  383.         if self._device_features & FEATURE_SET_POWER_PRICE == 1:
  384.             self._state_attrs[ATTR_POWER_PRICE] = None
  385.  
  386.     async def async_update(self):
  387.         """Fetch state from the device."""
  388.         # On state change the device doesn't provide the new state immediately.
  389.         if self._skip_update:
  390.             self._skip_update = False
  391.             return
  392.  
  393.         try:
  394.             state = await self.hass.async_add_executor_job(self._plug.status)
  395.             _LOGGER.debug("Got new state: %s", state)
  396.  
  397.             self._available = True
  398.             self._state = state.is_on
  399.             self._state_attrs.update(
  400.                 {ATTR_TEMPERATURE: state.temperature, ATTR_LOAD_POWER: state.load_power}
  401.             )
  402.  
  403.             if self._device_features & FEATURE_SET_POWER_MODE == 1 and state.mode:
  404.                 self._state_attrs[ATTR_POWER_MODE] = state.mode.value
  405.  
  406.             if self._device_features & FEATURE_SET_WIFI_LED == 1 and state.wifi_led:
  407.                 self._state_attrs[ATTR_WIFI_LED] = state.wifi_led
  408.  
  409.             if (
  410.                 self._device_features & FEATURE_SET_POWER_PRICE == 1
  411.                 and state.power_price
  412.             ):
  413.                 self._state_attrs[ATTR_POWER_PRICE] = state.power_price
  414.  
  415.         except DeviceException as ex:
  416.             if self._available:
  417.                 self._available = False
  418.                 _LOGGER.error("Got exception while fetching the state: %s", ex)
  419.  
  420.     async def async_set_power_mode(self, mode: str):
  421.         """Set the power mode."""
  422.         if self._device_features & FEATURE_SET_POWER_MODE == 0:
  423.             return
  424.  
  425.         await self._try_command(
  426.             "Setting the power mode of the power strip failed.",
  427.             self._plug.set_power_mode,
  428.             PowerMode(mode),
  429.         )
  430.  
  431.  
  432. class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
  433.     """Representation of a Chuang Mi Plug V1 and V3."""
  434.  
  435.     def __init__(self, name, plug, model, unique_id, channel_usb):
  436.         """Initialize the plug switch."""
  437.         name = f"{name} USB" if channel_usb else name
  438.  
  439.         if unique_id is not None and channel_usb:
  440.             unique_id = f"{unique_id}-usb"
  441.  
  442.         super().__init__(name, plug, model, unique_id)
  443.         self._channel_usb = channel_usb
  444.  
  445.         if self._model == MODEL_PLUG_V3:
  446.             self._device_features = FEATURE_FLAGS_PLUG_V3
  447.             self._state_attrs[ATTR_WIFI_LED] = None
  448.             if self._channel_usb is False:
  449.                 self._state_attrs[ATTR_LOAD_POWER] = None
  450.  
  451.     async def async_turn_on(self, **kwargs):
  452.         """Turn a channel on."""
  453.         if self._channel_usb:
  454.             result = await self._try_command(
  455.                 "Turning the plug on failed.", self._plug.usb_on
  456.             )
  457.         else:
  458.             result = await self._try_command(
  459.                 "Turning the plug on failed.", self._plug.on
  460.             )
  461.  
  462.         if result:
  463.             self._state = True
  464.             self._skip_update = True
  465.  
  466.     async def async_turn_off(self, **kwargs):
  467.         """Turn a channel off."""
  468.         if self._channel_usb:
  469.             result = await self._try_command(
  470.                 "Turning the plug on failed.", self._plug.usb_off
  471.             )
  472.         else:
  473.             result = await self._try_command(
  474.                 "Turning the plug on failed.", self._plug.off
  475.             )
  476.  
  477.         if result:
  478.             self._state = False
  479.             self._skip_update = True
  480.  
  481.     async def async_update(self):
  482.         """Fetch state from the device."""
  483.         # On state change the device doesn't provide the new state immediately.
  484.         if self._skip_update:
  485.             self._skip_update = False
  486.             return
  487.  
  488.         try:
  489.             state = await self.hass.async_add_executor_job(self._plug.status)
  490.             _LOGGER.debug("Got new state: %s", state)
  491.  
  492.             self._available = True
  493.             if self._channel_usb:
  494.                 self._state = state.usb_power
  495.             else:
  496.                 self._state = state.is_on
  497.  
  498.             self._state_attrs[ATTR_TEMPERATURE] = state.temperature
  499.  
  500.             if state.wifi_led:
  501.                 self._state_attrs[ATTR_WIFI_LED] = state.wifi_led
  502.  
  503.             if self._channel_usb is False and state.load_power:
  504.                 self._state_attrs[ATTR_LOAD_POWER] = state.load_power
  505.  
  506.         except DeviceException as ex:
  507.             if self._available:
  508.                 self._available = False
  509.                 _LOGGER.error("Got exception while fetching the state: %s", ex)
  510.  
  511.  
  512. class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
  513.     """Representation of a Xiaomi AirConditioning Companion."""
  514.  
  515.     def __init__(self, name, plug, model, unique_id):
  516.         """Initialize the acpartner switch."""
  517.         super().__init__(name, plug, model, unique_id)
  518.  
  519.         self._state_attrs.update({ATTR_TEMPERATURE: None, ATTR_LOAD_POWER: None})
  520.  
  521.     async def async_turn_on(self, **kwargs):
  522.         """Turn the socket on."""
  523.         result = await self._try_command(
  524.             "Turning the socket on failed.", self._plug.socket_on
  525.         )
  526.  
  527.         if result:
  528.             self._state = True
  529.             self._skip_update = True
  530.  
  531.     async def async_turn_off(self, **kwargs):
  532.         """Turn the socket off."""
  533.         result = await self._try_command(
  534.             "Turning the socket off failed.", self._plug.socket_off
  535.         )
  536.  
  537.         if result:
  538.             self._state = False
  539.             self._skip_update = True
  540.  
  541.     async def async_update(self):
  542.         """Fetch state from the device."""
  543.         # On state change the device doesn't provide the new state immediately.
  544.         if self._skip_update:
  545.             self._skip_update = False
  546.             return
  547.  
  548.         try:
  549.             state = await self.hass.async_add_executor_job(self._plug.status)
  550.             _LOGGER.debug("Got new state: %s", state)
  551.  
  552.             self._available = True
  553.             self._state = state.power_socket == "on"
  554.             self._state_attrs[ATTR_LOAD_POWER] = state.load_power
  555.  
  556.         except DeviceException as ex:
  557.             if self._available:
  558.                 self._available = False
  559.                 _LOGGER.error("Got exception while fetching the state: %s", ex)
  560.  
  561.  
  562. class XiaomiCurtainMotor(XiaomiPlugGenericSwitch):
  563.     """Representation of a Xiaomi Curtain Motor A1."""
  564.  
  565.     def __init__(self, name, plug, model, unique_id):
  566.         """Initialize the a1 motor."""
  567.         super().__init__(name, plug, model, unique_id)
  568.  
  569.         self._state_attrs.update({ATTR_TEMPERATURE: None})
  570.  
  571.     async def async_turn_on(self, **kwargs):
  572.         result = await self._try_command(
  573.             "Turning the socket on failed.", self._plug.set_motor_control, MotorControl(1),
  574.         )
  575.  
  576.         if result:
  577.             self._state = True
  578.             self._skip_update = True
  579.  
  580.     async def async_turn_off(self, **kwargs):
  581.         result = await self._try_command(
  582.             "Turning the socket off failed.", self._plug.set_motor_control, MotorControl(2),
  583.         )
  584.  
  585.         if result:
  586.             self._state = False
  587.             self._skip_update = True
  588.  
  589.     async def async_update(self):
  590.         """Fetch state from the device."""
  591.         # On state change the device doesn't provide the new state immediately.
  592.         if self._skip_update:
  593.             self._skip_update = False
  594.             return
  595.  
  596.         try:
  597.             state = await self.hass.async_add_executor_job(self._plug.status)
  598.             self._available = True
  599.             self._state = state.current_position > 0
  600.         except DeviceException as ex:
  601.             if self._available:
  602.                 self._available = False
  603.                 _LOGGER.error("Got exception while fetching the state: %s", ex)
Advertisement
Add Comment
Please, Sign In to add comment