Advertisement
grea09

Untitled

Mar 16th, 2019
392
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.57 KB | None | 0 0
  1. """Component to embed Broadlink devices."""
  2. import logging
  3.  
  4. from base64 import b64decode, b64encode
  5. import socket
  6.  
  7. import voluptuous as vol
  8.  
  9. from homeassistant.const import (CONF_NAME, CONF_DOMAIN, CONF_HOST, CONF_MAC,
  10.                                  CONF_TYPE, CONF_DISCOVERY, CONF_COMMAND_ON,
  11.                                  CONF_COMMAND_OFF, CONF_COMMAND, CONF_TIMEOUT,
  12.                                  ATTR_PACKET, ATTR_CONFIG, ATTR_ENTITY_ID,
  13.                                  ENTITY_ID_FORMAT, STATE_UNAVAILABLE)
  14. from homeassistant import config_entries
  15. from homeassistant.util import slugify
  16. from homeassistant.helpers import config_entry_flow
  17. from homeassistant.helpers.entity import ToggleEntity
  18. import homeassistant.helpers.config_validation as cv
  19. import homeassistant.helpers.device_registry as dr
  20.  
  21. REQUIREMENTS = ['broadlink==0.9.0']
  22.  
  23. _LOGGER = logging.getLogger(__name__)
  24.  
  25. DOMAIN = 'broadlink'
  26.  
  27. CONF_SP = 'sp'
  28. CONF_MP = 'mp'
  29. CONF_RM = 'rm'
  30. CONF_SLOTS = 'slots'
  31.  
  32. DEFAULT_TIMEOUT = 3  # In seconds
  33. DEFAULT_DOMAIN = 'switch'
  34. DEFAULT_SLOT_ENTITIES = [{CONF_NAME: name, CONF_DOMAIN: DEFAULT_DOMAIN}
  35.                          for name in map('slot_{}'.format, range(4))]
  36.  
  37. MODELS = {
  38.     0x0000: "SmartPlug 1",
  39.     0x2711: "SmartPlug 2",
  40.     0x2719: "Honeywell SmartPlug 2",
  41.     0x7919: "Honeywell SmartPlug 2",
  42.     0x271a: "Honeywell SmartPlug 2",
  43.     0x791a: "Honeywell SmartPlug 2",
  44.     0x2720: "SmartPlug Mini",
  45.     0x753e: "SmartPlug 3",
  46.     0x7D00: "OEM branded SmartPlug 3",
  47.     0x947a: "SmartPlug 3S",
  48.     0x9479: "SmartPlug 3S",
  49.     0x2728: "SmartPlug Mini 2",
  50.     0x2733: "OEM branded SmartPlug Mini",
  51.     0x273e: "OEM branded SmartPlug Mini",
  52.     0x7530: "OEM branded SmartPlug Mini 2",
  53.     0x7918: "OEM branded SmartPlug Mini 2",
  54.     0x2736: "SmartPlug Mini Plus",
  55.     0x2712: "Remote 2",
  56.     0x2737: "Remote Mini",
  57.     0x273d: "Remote Pro Phicomm",
  58.     0x2783: "Remote 2 Home Plus",
  59.     0x277c: "Remote 2 Home Plus GDT",
  60.     0x272a: "Remote 2 Pro Plus",
  61.     0x2787: "Remote 2 Pro Plus 2",
  62.     0x279d: "Remote 2 Pro Plus 3",
  63.     0x27a9: "Remote 2 Pro Plus_300",
  64.     0x278b: "Remote 2 Pro Plus BL",
  65.     0x2797: "Remote 2 Pro Plus HYC",
  66.     0x27a1: "Remote 2 Pro Plus R1",
  67.     0x27a6: "Remote 2 Pro PP",
  68.     0x278f: "Remote Mini Shate",
  69.     0x27c2: "Remote Mini 3",
  70.     0x2714: "A1",
  71.     0x4EB5: "MP1",
  72.     0x4EF7: "Honyar oem mp1",
  73.     0x4EAD: "Hysen controller",
  74.     0x2722: "SmartOne Alarm Kit",
  75.     0x4E4D: "Dooya DT360E (DOOYA_CURTAIN_V2)",
  76. }
  77.  
  78. RM_TYPES = ['rm', 'rm2', 'rm_mini', 'rm_pro_phicomm', 'rm2_home_plus',
  79.             'rm2_home_plus_gdt', 'rm2_pro_plus', 'rm2_pro_plus2',
  80.             'rm2_pro_plus_bl', 'rm_mini_shate']
  81. SP1_TYPES = ['sp1']
  82. SP2_TYPES = ['sp2', 'honeywell_sp2', 'sp3', 'spmini2', 'spminiplus']
  83. MP_TYPES = ['mp1']
  84.  
  85. SP_TYPES = SP1_TYPES + SP2_TYPES
  86.  
  87. BROADLINK_ENTITY = {
  88.     vol.Required(CONF_NAME): cv.string,
  89.     vol.Optional(CONF_DOMAIN, default=DEFAULT_DOMAIN): cv.string
  90. }
  91.  
  92. BROADLINK_DEVICE = {
  93.     vol.Required(CONF_HOST): cv.string,
  94.     vol.Required(CONF_MAC): cv.string,
  95.     vol.Required(CONF_TYPE): cv.string
  96. }
  97.  
  98. BROADLINK_COMMANDS = {
  99.     vol.Optional(CONF_COMMAND_ON, default=None): cv.string,
  100.     vol.Optional(CONF_COMMAND_OFF, default=None): cv.string
  101. }
  102.  
  103. BROADLINK_SP_SCHEMA = vol.Schema({
  104.     **BROADLINK_DEVICE,
  105.     **BROADLINK_ENTITY
  106. })
  107.  
  108. BROADLINK_MP_SCHEMA = vol.Schema({
  109.     **BROADLINK_DEVICE,
  110.     vol.Optional(CONF_SLOTS, default=DEFAULT_SLOT_ENTITIES):
  111.         vol.All(cv.ensure_list, [vol.Schema(BROADLINK_ENTITY)])
  112. })
  113.  
  114. BROADLINK_RM_SCHEMA = vol.Schema({
  115.     **BROADLINK_DEVICE,
  116.     vol.Optional(CONF_COMMAND, default=[]):
  117.         vol.All(cv.ensure_list, [vol.Schema({
  118.                 **BROADLINK_ENTITY, **BROADLINK_COMMANDS
  119.                 })])
  120. })
  121.  
  122.  
  123. CONFIG_SCHEMA = vol.Schema({
  124.     DOMAIN: vol.Schema({
  125.         vol.Optional(CONF_SP, default=[]): vol.All(cv.ensure_list,
  126.                                                    [BROADLINK_SP_SCHEMA]),
  127.         vol.Optional(CONF_MP, default=[]): vol.All(cv.ensure_list,
  128.                                                    [BROADLINK_MP_SCHEMA]),
  129.         vol.Optional(CONF_RM, default=[]): vol.All(cv.ensure_list,
  130.                                                    [BROADLINK_RM_SCHEMA]),
  131.         vol.Optional('discovery', default=True): cv.boolean,
  132.         vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int
  133.     }),
  134. })
  135.  
  136. STATE_LEARNING = 'learning'
  137.  
  138. SERVICE_LEARN = 'learn'
  139. SERVICE_SEND = 'send'
  140.  
  141. RM_LEARN_SERVICE_SCHEMA = vol.Schema({
  142.     vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
  143. })
  144.  
  145.  
  146. RM_SEND_SERVICE_SCHEMA = vol.Schema({
  147.     vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
  148.     vol.Required(ATTR_PACKET): cv.string,
  149. })
  150.  
  151.  
  152. async def _async_has_devices(hass):
  153.     """Return if there are devices that can be discovered."""
  154.     import broadlink
  155.  
  156.     def discover():
  157.         devs = broadlink.discover(timeout=1)
  158.  
  159.         return devs
  160.     return await hass.async_add_executor_job(discover)
  161.  
  162.  
  163. async def async_setup(hass, config):
  164.     """Set up the Broadlink component."""
  165.     conf = config.get(DOMAIN)
  166.  
  167.     hass.data[DOMAIN] = {}
  168.     hass.data[DOMAIN][ATTR_CONFIG] = conf
  169.  
  170.     if conf is not None:
  171.         hass.async_create_task(hass.config_entries.flow.async_init(
  172.             DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
  173.  
  174.     component.async_register_entity_service(
  175.         SERVICE_LEARN, RM_LEARN_SERVICE_SCHEMA,
  176.         'learn'
  177.     )
  178.  
  179.     component.async_register_entity_service(
  180.         SERVICE_SEND, RM_SEND_SERVICE_SCHEMA,
  181.         'send'
  182.     )
  183.  
  184.     return True
  185.  
  186.  
  187. async def async_setup_entry(hass, config_entry):
  188.     """Set up Broadlink from a config entry."""
  189.     devices = []
  190.  
  191.     config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
  192.  
  193.     # If discovery is defined and not disabled, discover devices
  194.     # If initialized from configure integrations, there's no config
  195.     # so we default here to True
  196.     if config_data is None or config_data[CONF_DISCOVERY]:
  197.         devices = await _async_has_devices(hass)
  198.         _LOGGER.info("Discovered {} Broadlink device(s)", len(devices))
  199.         for device in devices:
  200.             conf_device = {
  201.                 CONF_HOST: device.host,
  202.                 CONF_MAC: device.mac,
  203.                 CONF_TYPE: device.type.lower()
  204.             }
  205.             if device.type.lower() in SP_TYPES:
  206.                 config_data[CONF_SP] = conf_device
  207.             if device.type.lower() in MP_TYPES:
  208.                 config_data[CONF_MP] = conf_device
  209.             if device.type.lower() in RM_TYPES:
  210.                 config_data[CONF_RM] = conf_device
  211.             _LOGGER.debug("Succesfully added {}@{}",
  212.                           device.type, device.host)
  213.  
  214.     forward_setup = hass.config_entries.async_forward_entry_setup
  215.     hass.async_create_task(forward_setup(config_entry, 'light'))
  216.     hass.async_create_task(forward_setup(config_entry, 'switch'))
  217.  
  218.     return True
  219.  
  220.  
  221. async def async_unload_entry(hass, entry):
  222.     """Unload a config entry."""
  223.     forward_unload = hass.config_entries.async_forward_entry_unload
  224.     remove_lights = remove_switches = False
  225.     remove_lights = await forward_unload(entry, 'light')
  226.     remove_switches = await forward_unload(entry, 'switch')
  227.  
  228.     if remove_lights or remove_switches:
  229.         hass.data[DOMAIN].clear()
  230.         return True
  231.  
  232.     # We were not able to unload the platforms, either because there
  233.     # were none or one of the forward_unloads failed.
  234.     return False
  235.  
  236.  
  237. async def _learn(hass, call):
  238.     """Handle a learn command."""
  239.     entity_id = call.data.get(ATTR_ENTITY_ID, [])
  240.     # FIXME: get device ?
  241.     device = None
  242.     packet = device.learn()
  243.     if packet:
  244.         log_msg = "Received packet from {} is: {}".\
  245.                   format(entity_id, packet)
  246.         _LOGGER.info(log_msg)
  247.         hass.components.persistent_notification.async_create(
  248.             log_msg, title='Broadlink')
  249.     else:
  250.         hass.components.persistent_notification.async_create(
  251.           "Did not received any signal", title='Broadlink')
  252.  
  253.  
  254. async def _send(hass, call):
  255.     """Send a packet."""
  256.     entity_id = call.data.get(ATTR_ENTITY_ID, [])
  257.     # FIXME: get device ?
  258.     device = None
  259.     packets = call.data.get(ATTR_PACKET, [])
  260.     for packet in packets:
  261.         device.send(packet)
  262.  
  263.  
  264. class BroadlinkDevice(ToggleEntity):
  265.     """Representation of an generic Broadlink device."""
  266.  
  267.     def __init__(self, name, device, command_on=1, command_off=0, slot=None):
  268.         """Initialize the device."""
  269.         self.entity_id = ENTITY_ID_FORMAT.format(slugify(name))
  270.         self._name = name
  271.         self._state = False
  272.         if isinstance(command_on, str):
  273.             self._command_on = b64decode(command_on)
  274.             self._command_off = (b64decode(command_off) if command_off
  275.                                  else self._command_on)
  276.         elif isinstance(command_off, str):
  277.             self._command_off = 0
  278.         self._device = device
  279.         self._slot = slot
  280.         if slot and self._device.type in MP_TYPES:
  281.             slot = 1
  282.  
  283.     @property
  284.     def name(self):
  285.         """Return the name of the device."""
  286.         return self._name
  287.  
  288.     @property
  289.     def unique_id(self):
  290.         """Return a unique ID."""
  291.         return self._device.mac
  292.  
  293.     @property
  294.     def device_info(self):
  295.         """Return information about the device."""
  296.         return {
  297.             "name": self.name,
  298.             "model": MODELS[self._device.devtype],
  299.             "manufacturer": 'Broadlink',
  300.             "connections": {
  301.                 (dr.CONNECTION_NETWORK_MAC, self._device.mac),
  302.                 (dr.CONNECTION_NETWORK_IP, self._device.host[0]),
  303.             },
  304.         }
  305.  
  306.     @property
  307.     def state(self):
  308.         """Defaulting to power"""
  309.         return self._state
  310.  
  311.     def learn(self):
  312.         """Start learning if device type is RM"""
  313.         if self._device.type in RM_TYPES:
  314.             self._auth()
  315.             self._state = STATE_LEARNING
  316.             self._device.enter_learning()
  317.             _LOGGER.info("Press the key you want Home Assistant to learn")
  318.             self.update()
  319.             packet = self._state
  320.             if packet:
  321.                 return b64encode(packet).decode('utf8')
  322.             _LOGGER.error("Did not received any signal")
  323.             self.schedule_update_ha_state()
  324.         return None
  325.  
  326.     def turn_on(self, **kwargs):
  327.         """Turn the device on."""
  328.         if self._sendpacket(self._command_on):
  329.             self._state = True
  330.             self.schedule_update_ha_state()
  331.  
  332.     def turn_off(self, **kwargs):
  333.         """Turn the device off."""
  334.         if self._sendpacket(self._command_off):
  335.             self._state = False
  336.             self.schedule_update_ha_state()
  337.  
  338.     def send(self, packet, retry=5):
  339.         """Send packet to device."""
  340.         if packet is None:
  341.             _LOGGER.warn("Empty packet")
  342.             return True
  343.         try:
  344.             self._send(packet)
  345.         except (socket.timeout, ValueError) as error:
  346.             if retry < 1:
  347.                 _LOGGER.error("Error during sending a packet: {}", error)
  348.                 return False
  349.             if not self._auth():
  350.                 return False
  351.             return self._sendpacket(packet, retry-1)
  352.         return True
  353.  
  354.     def _send(self, packet):
  355.         """Defaulting to power"""
  356.         self._auth()
  357.         if self._device.type in MP_TYPES:
  358.             self._state = self._device.set_power(self._slot, packet)
  359.         elif self._device.type in RM_TYPES:
  360.             self._state = self._device.send_data(packet)
  361.         else:
  362.             self._state = self._device.set_power(packet)
  363.  
  364.     def auth(self, retry=5):
  365.         try:
  366.             auth = self._device.auth()
  367.         except socket.timeout:
  368.             auth = False
  369.             if retry < 1:
  370.                 _LOGGER.error("Timeout during authorization")
  371.         if not auth and retry > 0:
  372.             return self._auth(retry-1)
  373.         return auth
  374.  
  375.     def update(self):
  376.         """Defaulting to power"""
  377.         self._auth()
  378.         state = STATE_UNAVAILABLE
  379.         if self._device.type in MP_TYPES:
  380.             state = self._device.check_power(self._slot)
  381.         elif self._device.type in RM_TYPES:
  382.             state = self._device.check_data()
  383.         else:
  384.             state = self._device.check_power()
  385.         self._state = state
  386.  
  387.  
  388. config_entry_flow.register_discovery_flow(DOMAIN,
  389.                                           'Broadlink',
  390.                                           _async_has_devices,
  391.                                           config_entries.CONN_CLASS_LOCAL_POLL)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement