fr.immotronic.ubikit.pems.enocean.impl.PairingManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for fr.immotronic.ubikit.pems.enocean.impl.PairingManagerImpl.java

Source

/*
 * Copyright (c) Immotronic, 2012
 *
 * Contributors:
 *
 *     Lionel Balme (lbalme@immotronic.fr)
 *     Kevin Planchet (kplanchet@immotronic.fr)
 *
 * This file is part of ubikit-core, a component of the UBIKIT project.
 *
 * This software is a computer program whose purpose is to host third-
 * parties applications that make use of sensor and actuator networks.
 *
 * This software is governed by the CeCILL-C license under French law and
 * abiding by the rules of distribution of free software.  You can  use,
 * modify and/ or redistribute the software under the terms of the CeCILL-C
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * As a counterpart to the access to the source code and  rights to copy,
 * "http://www.cecill.info".
 *
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability.
 *
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or
 * data to be ensured and,  more generally, to use and operate it in the
 * same conditions as regards security.
 *
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL-C license and that you accept its terms.
 *
 * CeCILL-C licence is fully compliant with the GNU Lesser GPL v2 and v3.
 *
 */

package fr.immotronic.ubikit.pems.enocean.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.json.JSONException;
import org.json.JSONObject;
import org.ubikit.Logger;
import org.ubikit.PhysicalEnvironmentItem.Type;
import org.ubikit.event.EventGate;
import org.ubikit.pem.event.AddItemEvent;
import org.ubikit.pem.event.EnterPairingModeEvent;
import org.ubikit.pem.event.ExitPairingModeEvent;
import org.ubikit.pem.event.ItemAddingFailedEvent;
import org.ubikit.pem.event.NewItemEvent;
import org.ubikit.pem.event.NewItemEvent.CapabilitySelection;
import org.ubikit.pem.event.UnsupportedNewItemEvent;

//import fr.immotronic.license.LicenseManager;
import fr.immotronic.ubikit.pems.enocean.EnoceanEquipmentProfileV20;
import fr.immotronic.ubikit.pems.enocean.SensorActuatorProfile;
import fr.immotronic.ubikit.pems.enocean.data.EEPD201xxData;

/*
 * 
 * TODO: Consider adding a SelectionErrorEvent (for SelectionEvent matching
 * no element in the toPair map, and SelectionApprovedEvent to give a positive
 * feedback to the SelectionEvent sender.
 * 
 *  TODO: Consider adding a PairingCancelledEvent management, when the user wish
 *  to dismissed an ongoing pairing.
*/

public final class PairingManagerImpl
        implements AddItemEvent.Listener, EnterPairingModeEvent.Listener, ExitPairingModeEvent.Listener {
    private static final long CLEANING_THREAD_SLEEPING_PERIOD = 60; // 1 hour
    private static final long PAIRING_TIMEOUT = 600000; // 10 minutes

    private final class PairingEntry {
        private long arrivalTime;
        private int enoceanUID;
        private String itemUID;
        private EnoceanEquipmentProfileV20 eep;
        private int manufacturerUID;
        private JSONObject properties = null;

        PairingEntry(int enoceanUID, EnoceanEquipmentProfileV20 eep, int manufacturerUID) {
            arrivalTime = System.currentTimeMillis();
            this.enoceanUID = enoceanUID;
            this.itemUID = EnoceanDeviceImpl.makeEnoceanItemUID(enoceanUID);
            this.eep = eep;
            this.manufacturerUID = manufacturerUID;
        }

        public boolean isExpired(long currentTime) {
            return (arrivalTime + PAIRING_TIMEOUT) < currentTime;
        }
    }

    private final DeviceManager deviceManager;
    //private final LicenseManager licenseManager;
    private final EventGate eventGateToHigherAbstractionModelLevels;
    private final String pemUID;
    private final Map<String, PairingEntry> toPair;
    private boolean pairingMode;

    private class CleaningThread implements Runnable {
        public void run() {
            ArrayList<String> toBeRemoved = new ArrayList<String>();

            if (LC.debug) {
                Logger.debug(LC.gi(), this, "cleaning task start...");
            }

            // Get current system time
            long referenceTime = System.currentTimeMillis();

            synchronized (toPair) {
                Iterator<PairingEntry> entries = toPair.values().iterator();

                // For each entry of arrival times
                while (entries.hasNext()) {
                    PairingEntry entry = entries.next();
                    if (entry.isExpired(referenceTime)) // if the entry expired
                    {
                        // Mark it as "entry to remove"
                        toBeRemoved.add(entry.itemUID);

                        if (LC.debug) {
                            Logger.debug(LC.gi(), this,
                                    "cleaning thread mark " + entry.enoceanUID + " item to be removed.");
                        }
                    }
                }
            }

            // Remove marked entry
            for (String key : toBeRemoved) {
                toPair.remove(key);
            }

            // Cleaning the toBeRemoved list. It will be ready for the next cleaning.
            toBeRemoved.clear();

            if (LC.debug) {
                Logger.debug(LC.gi(), this, "cleaning task stopped.");
            }
        }
    }

    public PairingManagerImpl(DeviceManager deviceManager,
            /*LicenseManager licenseManager,*/ EventGate eventGateToHigherAbstractionModelLevels,
            ScheduledExecutorService executorService, String pemUID) {
        pairingMode = false;
        this.pemUID = pemUID;
        toPair = Collections.synchronizedMap(new HashMap<String, PairingEntry>());
        executorService.scheduleAtFixedRate(new CleaningThread(), CLEANING_THREAD_SLEEPING_PERIOD,
                CLEANING_THREAD_SLEEPING_PERIOD, TimeUnit.MINUTES);

        this.deviceManager = deviceManager;
        //this.licenseManager = licenseManager;
        this.eventGateToHigherAbstractionModelLevels = eventGateToHigherAbstractionModelLevels;
        eventGateToHigherAbstractionModelLevels.addListener(this);
    }

    public boolean getPairingMode() {
        return pairingMode;
    }

    /**
     * 
     * @param telegram
     * @return false if the telegram was not a teach-in telegram and should be ignore. Return true otherwise.
     */
    public boolean pairNewDevice(EnoceanTelegram telegram) {
        //
        // PRE-CONDITION: This method is called ONLY if the PEM pairing mode is ON.
        //

        if (LC.debug) {
            Logger.debug(LC.gi(), this, "Try to pair a new device");
        }

        int uid = telegram.getTransmitterId(); // Get the device Enocean UID
        byte[] databytes = telegram.getData().getBytes();
        PairingEntry pairingEntry = null;

        // Determine the ORG parameter in the telegram, and instantiate the appropriate PairingEntry object 
        switch (telegram.getRorgID()) {
        case RORG_RPS:
            // Manufacturer could not be known.
            if ((databytes[3] & 0xFF) >= 0xc0) {
                // The device is a Window Handle
                pairingEntry = new PairingEntry(uid, EnoceanEquipmentProfileV20.EEP_05_10_00, 0);
            } else {
                // An pairing entry with a generic EEP information is created. The final user will have to
                // indicate, via the HCI, the exact EEP to use for this sensor.
                pairingEntry = new PairingEntry(uid, EnoceanEquipmentProfileV20.EEP_05_xx_xx, 0);
                break;
            }
            break;

        case RORG_1BS:
            // For ORG == 1BS, only on profile match: EEP_06_00_01. Manufacturer could not be known.
            // The data byte 3, bit 3 indicate a teach-in telegram. Check it.
            if ((databytes[3] & 0x8) == 0x0) {
                // This is a teach-in telegram
                pairingEntry = new PairingEntry(uid, EnoceanEquipmentProfileV20.EEP_06_00_01, 0);
                break;
            }

            // This is not a teach-in telegram, stop here.
            if (LC.debug) {
                Logger.debug(LC.gi(), this, "This was not a 1BS teach-in telegram.");
            }
            return false;

        case RORG_4BS:
            // For ORG == 4BS, profile could be determined using data bytes 2 & 3. Manufacturer ID is on data bytes 2 & 1
            // The data byte 0, bit 3 indicate a teach-in telegram. Check it.
            if ((databytes[0] & 0x8) == 0x0) {
                // This is a teach-in telegram
                if ((databytes[0] & 0x80) == 0x80) // If bit 7 is 0, function, type and manufacturer info are provided
                {
                    int function = ((databytes[3] & 0xff) >> 2) & 0x3f;
                    int type = ((databytes[3] & 0x3) << 5) + ((databytes[2] & 0xff) >> 3) & 0x1f;
                    int manufacturerID = ((databytes[2] & 0x7) << 8) + (databytes[1] & 0xff);

                    if (LC.debug) {
                        Logger.debug(LC.gi(), this,
                                "4BS teach-in telegram: function=" + Integer.toHexString(function) + ", type="
                                        + Integer.toHexString(type) + ", manufacturer=" + manufacturerID);
                    }

                    // Try to figure out witch is the matching EEP.
                    EnoceanEquipmentProfileV20 eep = EnoceanEquipmentProfileV20.selectEEP(0x7, function, type);
                    if (eep == null) {
                        // No EEP match: an UnsupportedDeviceEvent message and processing stop here. 
                        if (LC.debug) {
                            Logger.debug(LC.gi(), this,
                                    "4BS teach-in telegram function & types information does not match a valid EEP: "
                                            + telegram);
                        }
                        UnsupportedNewItemEvent uni = new UnsupportedNewItemEvent(
                                EnoceanDeviceImpl.makeEnoceanItemUID(telegram.getTransmitterId()), pemUID,
                                Type.SENSOR, null);
                        eventGateToHigherAbstractionModelLevels.postEvent(uni);
                        pairingEntry = new PairingEntry(uid, eep, manufacturerID);
                        toPair.put(pairingEntry.itemUID, pairingEntry);
                        return false;
                    } else {
                        if (LC.debug) {
                            Logger.debug(LC.gi(), this, "Matching EEP is " + eep.name());
                        }
                    }

                    pairingEntry = new PairingEntry(uid, eep, manufacturerID);
                    break;
                } else {
                    // Function and type cannot be determined. That's happen when sensor manufacturer does not conformed to Enocean Standard.
                    if (LC.debug) {
                        Logger.debug(LC.gi(), this,
                                "4BS teach-in telegram does not contains function & types information: "
                                        + telegram);
                    }

                    // An pairing entry without EEP information is created. The final user will have to
                    // indicate, via the HCI, the EEP to use for this sensor.
                    pairingEntry = new PairingEntry(uid, EnoceanEquipmentProfileV20.NONE, 0);
                    break;
                }
            }

            // This is not a teach-in telegram, stop here.
            if (LC.debug) {
                Logger.debug(LC.gi(), this, "This was not a 4BS teach-in telegram.");
            }
            return false;
        case RORG_UTE:
            // For ORG == UTE, data byte 0 provide RORG of EEP. The profile could be determined using data bytes 1 & 2. Manufacturer ID is on data bytes 3 & 4.
            // The data byte 6, bit 3..0 indicate a teach-in query telegram. Check it.
            if ((databytes[6] & 0xf) == 0x0) {
                // Data byte 6, bit 5..4 indicates if it's a teach-in request or a teach-in deletion request. Check it.
                if ((databytes[6] & 0x10) == 0x0) {
                    // Yes, it is a teach-in request.
                    // Test if the devide need a teach-in response
                    if ((databytes[6] & 0x50) == 0x0)
                        Logger.warn(LC.gi(), this,
                                "The device you're trying to pair expect a Teach-In-Response message but this is not yet implemented. The pairing could failed.");

                    // NB : DB6 contains also info on Uni/Bidirectional information & info on the need of a response.
                    //       DB5 contains the number of channel supported by the device.

                    int org = databytes[0] & 0xff;
                    int function = databytes[1] & 0xff;
                    int type = databytes[2] & 0xff;
                    int manufacturerID = ((databytes[3] & 0x7) << 8) + (databytes[4] & 0xff);

                    // Try to figure out witch is the matching EEP.
                    EnoceanEquipmentProfileV20 eep = EnoceanEquipmentProfileV20.selectEEP(org, function, type);
                    if (eep == null) {
                        // No EEP match: an UnsupportedDeviceEvent message and processing stop here. 
                        if (LC.debug) {
                            Logger.debug(LC.gi(), this,
                                    "UTE teach-in telegram org & function & types information does not match a valid EEP: "
                                            + telegram);
                        }
                        UnsupportedNewItemEvent uni = new UnsupportedNewItemEvent(
                                EnoceanDeviceImpl.makeEnoceanItemUID(telegram.getTransmitterId()), pemUID,
                                Type.SENSOR, null);
                        eventGateToHigherAbstractionModelLevels.postEvent(uni);
                        pairingEntry = new PairingEntry(uid, eep, manufacturerID);
                        toPair.put(pairingEntry.itemUID, pairingEntry);
                        return false;
                    } else {
                        if (LC.debug) {
                            Logger.debug(LC.gi(), this, "Matching EEP is " + eep.name());
                        }
                    }

                    pairingEntry = new PairingEntry(uid, eep, manufacturerID);
                    break;
                }
            }

            // This was not a correct teach-in telegram, stop here.
            if (LC.debug) {
                Logger.debug(LC.gi(), this, "This was not a UTE teach-in query telegram.");
            }
            return false;
        default:
            // This telegram is not yet supported: an UnsupportedDeviceEvent message and processing stop here.
            if (LC.debug) {
                Logger.debug(LC.gi(), this, "Unmanaged telegram type: " + telegram);
            }
            UnsupportedNewItemEvent uni = new UnsupportedNewItemEvent(
                    EnoceanDeviceImpl.makeEnoceanItemUID(telegram.getTransmitterId()), pemUID, Type.SENSOR, null);
            eventGateToHigherAbstractionModelLevels.postEvent(uni);
            return true;
        }

        // The device can be associated to an existing one in some case (EEP 07-20-10 and EEP 07-20-11 for instance) so we check this.
        if (isDeviceExistWithAnotherUID(pairingEntry)) {
            return true;
        }

        // Adding the pairing entry to the "toPair" queue
        toPair.put(pairingEntry.itemUID, pairingEntry);

        // and send the confirmation request message
        String[] capabilities = null;
        CapabilitySelection cs = null;
        //if((pairingEntry.eep.getFunction() == 0 && pairingEntry.eep != EnoceanEquipmentProfileV20.EEP_06_00_01) || (pairingEntry.eep.getFunction() == 0x20)) {
        //   cs = CapabilitySelection.SINGLE;
        switch (pairingEntry.eep) {
        case EEP_05_xx_xx:
            capabilities = new String[3];
            capabilities[0] = EnoceanEquipmentProfileV20.EEP_05_02_01.name();
            capabilities[1] = EnoceanEquipmentProfileV20.EEP_05_04_01.name();
            capabilities[2] = EnoceanEquipmentProfileV20.ELTAKO_FRW_WS_SMOKE_ALARM.name();
            cs = CapabilitySelection.SINGLE;
            break;
        case EEP_07_20_10:
        case EEP_07_20_11:
            capabilities = new String[1];
            capabilities[0] = SensorActuatorProfile.INTESIS_BOX_DK_RC_ENO_1i1iC_DEVICE.name();
            cs = CapabilitySelection.SINGLE;
            break;
        case EEP_D2_01_11:
            capabilities = new String[2];
            capabilities[0] = SensorActuatorProfile.EEP_D2_01_11.name() + "?mode=" + EEPD201xxData.Mode.RELAY;
            capabilities[1] = SensorActuatorProfile.EEP_D2_01_11.name() + "?mode=" + EEPD201xxData.Mode.MOTOR;
            cs = CapabilitySelection.SINGLE;
            break;
        case NONE:
            capabilities = new String[3];
            capabilities[0] = EnoceanEquipmentProfileV20.EEP_07_06_01.name();
            capabilities[1] = EnoceanEquipmentProfileV20.EEP_07_10_03.name();
            capabilities[2] = EnoceanEquipmentProfileV20.EEP_A5_12_00.name();
            cs = CapabilitySelection.SINGLE;
            break;
        default:
            //capabilities = new String[0];
            capabilities = new String[1];
            capabilities[0] = pairingEntry.eep.name();
            cs = CapabilitySelection.NO;
            break;
        }
        //}

        // Defining type (SENSOR or SENSOR_AND_ACTUATOR)
        Type type = getSensorTypeFromEEP(pairingEntry.eep);

        NewItemEvent ni = new NewItemEvent(EnoceanDeviceImpl.makeEnoceanItemUID(uid), pemUID, type, capabilities,
                cs);
        eventGateToHigherAbstractionModelLevels.postEvent(ni);

        return true;
    }

    private Type getSensorTypeFromEEP(EnoceanEquipmentProfileV20 eep) {
        Type type = Type.SENSOR;

        switch (eep) {
        case EEP_07_20_11:
        case EEP_07_20_10:
        case EEP_D2_01_00:
        case EEP_D2_01_02:
        case EEP_D2_01_06:
        case EEP_D2_01_11:
            type = Type.SENSOR_AND_ACTUATOR;
            break;
        default:
            break;
        }

        return type;
    }

    private boolean isDeviceExistWithAnotherUID(PairingEntry pairingEntry) {
        try {
            if (pairingEntry.eep == EnoceanEquipmentProfileV20.EEP_07_20_10
                    || pairingEntry.eep == EnoceanEquipmentProfileV20.EEP_07_20_11) {
                PairingEntry existingEntry = getPairingEntryWithSameBaseUID(pairingEntry.enoceanUID);

                if (existingEntry != null) {
                    // INVARIANT HERE : existingEntry.properties could not be null : at least, a first property has been created
                    // when the first EEP of this device was received.
                    existingEntry.properties.put(pairingEntry.eep.name(),
                            Integer.toHexString(pairingEntry.enoceanUID));
                    return true;
                } else {
                    JSONObject properties = pairingEntry.properties;
                    if (properties == null) {
                        properties = new JSONObject();
                        pairingEntry.properties = properties;
                    }

                    properties.put(pairingEntry.eep.name(), Integer.toHexString(pairingEntry.enoceanUID));
                }
            }
        } catch (JSONException e) {
            Logger.error(LC.gi(), this,
                    "isDeviceExistWithAnotherUID() : Trouble while building JSON Object. SHOULD NEVER HAPPEN. Check code.",
                    e);
            return false;
        } catch (Exception e) {
            Logger.error(LC.gi(), this, "isDeviceExistWithAnotherUID() : SHOULD NEVER HAPPEN. Check code.", e);
            return false;
        }

        if (LC.debug) {
            Logger.debug(LC.gi(), this, "isDeviceExistWithAnotherUID() will return false. Entry was "
                    + pairingEntry.enoceanUID + "/" + pairingEntry.eep);
        }
        return false;
    }

    private PairingEntry getPairingEntryWithSameBaseUID(int enoceanUID) {
        int baseUID = enoceanUID & 0x80;
        for (PairingEntry pe : toPair.values()) {
            if ((pe.enoceanUID & 0x80) == baseUID) {
                return pe;
            }
        }

        return null;
    }

    @Override
    public void onEvent(AddItemEvent event) {
        PairingEntry pe = toPair.remove(event.getSourceItemUID());

        if (pe != null) {
            String capability = null;
            String params = null;
            String[] capabilities = event.getCapabilities();
            if (capabilities == null || capabilities.length == 0) {
                capability = pe.eep.name();
            } else {
                capability = capabilities[0];
                int paramStringIndex = capability.indexOf('?');
                if (paramStringIndex > 0) {
                    params = capability.substring(paramStringIndex + 1);
                    capability = capability.substring(0, paramStringIndex);
                }
            }

            if (pe.properties == null) {
                pe.properties = event.getUserProperties();
            } else {
                Iterator<?> itr = event.getUserProperties().keys();
                while (itr.hasNext()) {
                    String key = (String) itr.next();
                    try {
                        pe.properties.put(key, event.getUserProperties().get(key));
                    } catch (JSONException e) {
                        Logger.error(LC.gi(), this,
                                "isDeviceExistWithAnotherUID() : Trouble while building JSON Object. SHOULD NEVER HAPPEN. Check code.",
                                e);
                    }
                }
            }

            // TODO: sparer la partie "paramtre" de la partie EEP de la capability, et la transmettre  createNewDevice. 

            try {
                EnoceanEquipmentProfileV20 eep = EnoceanEquipmentProfileV20.valueOf(capability);
                if (getSensorTypeFromEEP(eep) != Type.SENSOR_AND_ACTUATOR)
                    deviceManager.createNewDevice(pe.enoceanUID, eep, pe.manufacturerUID, pe.properties,
                            new DeviceParameters(params));

                Logger.debug(LC.gi(), this,
                        "Confirmation for " + event.getSourceItemUID() + " as sensor with profile " + eep);
            } catch (IllegalArgumentException e) {
            }

            try {
                SensorActuatorProfile sap = SensorActuatorProfile.valueOf(capability);
                deviceManager.createNewDevice(pe.enoceanUID, sap, pe.manufacturerUID, pe.properties,
                        new DeviceParameters(params));
                Logger.debug(LC.gi(), this,
                        "Confirmation for " + event.getSourceItemUID() + " as sensor/actuator with profile " + sap);
            } catch (IllegalArgumentException e) {
            }
            return;
        }

        ItemAddingFailedEvent iaf = new ItemAddingFailedEvent(event.getSourceItemUID(), "UNKNOWN_ITEM", 1);
        eventGateToHigherAbstractionModelLevels.postEvent(iaf);
    }

    @Override
    public void onEvent(ExitPairingModeEvent event) {
        pairingMode = false;
    }

    @Override
    public void onEvent(EnterPairingModeEvent event) {
        //licenseManager.checkLicenceFile();
        pairingMode = true;
    }
}