/* * Copyright 2013-2017 Erudika. https://erudika.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.para.iot; import com.eaio.uuid.UUID; import com.erudika.para.DestroyListener; import com.erudika.para.Para; import com.erudika.para.core.Thing; import com.erudika.para.core.utils.ParaObjectUtils; import com.erudika.para.utils.Config; import com.erudika.para.utils.Utils; import com.microsoft.azure.eventhubs.EventData; import com.microsoft.azure.eventhubs.EventHubClient; import com.microsoft.azure.eventhubs.PartitionReceiver; import com.microsoft.azure.iot.service.sdk.DeliveryAcknowledgement; import com.microsoft.azure.iot.service.sdk.Device; import com.microsoft.azure.iot.service.sdk.IotHubServiceClientProtocol; import com.microsoft.azure.iot.service.sdk.RegistryManager; import com.microsoft.azure.iot.service.sdk.ServiceClient; import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.Map; import java.util.function.Consumer; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Azure IoT client. * @author Alex Bogdanovski [alex@erudika.com] */ public class AzureIoTService implements IoTService { private static final Logger logger = LoggerFactory.getLogger(AzureIoTService.class); private static final int MAX_MESSAGES = Config.getConfigInt("azure.iot_max_messages", 10); private static final int PARTITIONS_COUNT = Config.getConfigInt("azure.iot_partitions", 2); private static final String SERVICE_HOSTNAME = Config.getConfigParam("azure.iot_hostname", ""); private static final String SERVICE_ACCESS_KEY = Config.getConfigParam("azure.iot_access_key", ""); private static final String SERVICE_CONN_STR = "HostName=" + SERVICE_HOSTNAME + ";SharedAccessKeyName=iothubowner;SharedAccessKey=" + SERVICE_ACCESS_KEY; private static final String EVENTHUB_NAME = Config.getConfigParam("azure.iot_eventhub_name", ""); private static final String EVENTHUB_ENDPOINT = Config.getConfigParam("azure.iot_eventhub_endpoint", ""); private static final String EVENTHUB_CONN_STR = "Endpoint=" + EVENTHUB_ENDPOINT + ";EntityPath=" + EVENTHUB_NAME + ";SharedAccessKeyName=iothubowner;SharedAccessKey=" + SERVICE_ACCESS_KEY; private ServiceClient serviceClient = null; private RegistryManager registryManager = null; /** * No-args constructor. */ public AzureIoTService() { if (!StringUtils.isBlank(SERVICE_ACCESS_KEY)) { if (!StringUtils.isBlank(EVENTHUB_ENDPOINT)) { final ArrayList<EventHubClient> recievers = new ArrayList<EventHubClient>(); for (int i = 0; i < PARTITIONS_COUNT; i++) { recievers.add(receiveEventsAsync(Integer.toString(i))); } Para.addDestroyListener(new DestroyListener() { public void onDestroy() { for (EventHubClient recvr : recievers) { recvr.close(); } } }); } try { registryManager = RegistryManager.createFromConnectionString(SERVICE_CONN_STR); } catch (Exception ex) { logger.warn("Couldn't initialize Azure registry manager: {}", ex.getMessage()); } } } protected ServiceClient getClient() { try { if (serviceClient != null) { return serviceClient; } serviceClient = ServiceClient.createFromConnectionString(SERVICE_CONN_STR, IotHubServiceClientProtocol.AMQPS); serviceClient.open(); Para.addDestroyListener(new DestroyListener() { public void onDestroy() { shutdownClient(); } }); } catch (Exception ex) { logger.warn("Couldn't create Azure IoT service client: {}", ex.getMessage()); } return serviceClient; } /** * Stops the client and releases resources. * <b>There's no need to call this explicitly!</b> */ protected void shutdownClient() { if (serviceClient != null) { try { serviceClient.close(); serviceClient = null; } catch (Exception ex) { logger.warn("Couldn't close Azure IoT service client: {}", ex.getMessage()); } } } @Override public Thing createThing(Thing thing) { if (thing == null || StringUtils.isBlank(thing.getName()) || StringUtils.isBlank(thing.getAppid()) || StringUtils.isBlank(SERVICE_ACCESS_KEY) || existsThing(thing)) { return null; } try { thing.setId(Utils.getNewId()); String id = cloudIDForThing(thing); Device device = Device.createFromId(id, null, null); device = registryManager.addDevice(device); logger.debug("Thing {} created on Azure.", id); thing.setServiceBroker("Azure"); thing.getDeviceMetadata().put("thingId", id); thing.getDeviceMetadata().put("thingName", thing.getName()); thing.getDeviceMetadata().put("thingGenId", device.getGenerationId()); thing.getDeviceMetadata().put("status", device.getStatus()); thing.getDeviceMetadata().put("primaryKey", device.getPrimaryKey()); thing.getDeviceMetadata().put("secondaryKey", device.getSecondaryKey()); thing.getDeviceMetadata().put("lastActivity", device.getLastActivityTime()); thing.getDeviceMetadata().put("connectionState", device.getConnectionState()); thing.getDeviceMetadata().put("connectionString", "HostName=" + SERVICE_HOSTNAME + ";DeviceId=" + id + ";SharedAccessKey=" + device.getPrimaryKey()); } catch (Exception e) { logger.warn(null, e); } return thing; } @Override public Map<String, Object> readThing(Thing thing) { // no operation - we're polling for messages in the background // instead of updating on read as in AWSIoTService.java return null; } @Override public void updateThing(Thing thing) { if (thing == null || StringUtils.isBlank(thing.getId()) || StringUtils.isBlank(thing.getAppid()) || StringUtils.isBlank(SERVICE_ACCESS_KEY)) { return; } try { Date now = new Date(); com.microsoft.azure.iot.service.sdk.Message messageToSend; messageToSend = new com.microsoft.azure.iot.service.sdk.Message(ParaObjectUtils.getJsonWriterNoIdent(). writeValueAsBytes(thing.getDeviceState())); messageToSend.setDeliveryAcknowledgement(DeliveryAcknowledgement.None); messageToSend.setMessageId(new UUID().toString()); messageToSend.setExpiryTimeUtc(new Date(now.getTime() + 24 * 60 * 60 * 1000)); messageToSend.setCorrelationId(new UUID().toString()); // messageToSend.setUserId(thing.getCreatorid()); messageToSend.clearCustomProperties(); getClient().send(cloudIDForThing(thing), messageToSend); } catch (Exception e) { logger.warn("Couldn't create thing: {}", e.getMessage()); } } @Override public void deleteThing(Thing thing) { if (thing == null || StringUtils.isBlank(thing.getId()) || StringUtils.isBlank(thing.getAppid()) || StringUtils.isBlank(SERVICE_ACCESS_KEY)) { return; } try { String id = cloudIDForThing(thing); registryManager.removeDeviceAsync(id); logger.debug("Thing {} removed from Azure.", id); } catch (Exception e) { logger.warn("Couldn't delete thing: {}", e.getMessage()); } } @Override public boolean existsThing(Thing thing) { if (thing == null || StringUtils.isBlank(thing.getId()) || StringUtils.isBlank(thing.getAppid()) || StringUtils.isBlank(SERVICE_ACCESS_KEY)) { return false; } try { return registryManager.getDevice(cloudIDForThing(thing)) != null; } catch (Exception e) { return false; } } private static EventHubClient receiveEventsAsync(final String partitionId) { EventHubClient client = null; try { client = EventHubClient.createFromConnectionStringSync(EVENTHUB_CONN_STR); client.createReceiver(EventHubClient.DEFAULT_CONSUMER_GROUP_NAME, partitionId, Instant.now()). thenAccept(new Receiver(partitionId)); } catch (Exception e) { logger.warn("Couldn't start receiving messages from Azure cloud: {}", e.getMessage()); } return client; } private String cloudIDForThing(Thing thing) { return thing.getAppid().concat(Config.SEPARATOR).concat(thing.getId()); } private static Thing thingFromCloudID(String id) { if (!StringUtils.isBlank(id) && id.contains(Config.SEPARATOR)) { String[] parts = id.split(Config.SEPARATOR); Thing t = new Thing(parts[1]); t.setServiceBroker("Azure"); t.setAppid(parts[0]); return t; } return null; } /** * Receiver class. */ static class Receiver implements Consumer<PartitionReceiver> { private String partitionId; Receiver(String partitionId) { this.partitionId = partitionId; } @Override public void accept(PartitionReceiver receiver) { while (true) { try { Iterable<EventData> receivedEvents = receiver.receive(MAX_MESSAGES).get(); int batchSize = 0; if (receivedEvents != null) { for (EventData receivedEvent : receivedEvents) { String deviceId = (String) receivedEvent.getProperties().get("iothub-connection-device-id"); Map<String, Object> deviceState = null; try { deviceState = ParaObjectUtils.getJsonReader(Map.class).readValue(receivedEvent.getBody()); logger.debug("Message received from Azure: {}", deviceState); } catch (Exception e) { } if (deviceState != null) { Thing t = thingFromCloudID(deviceId); if (t != null) { t.setDeviceState(deviceState); Para.getDAO().update(t.getAppid(), t); } } batchSize++; } } logger.debug("Received {} messages from Azure for partition {}.", batchSize, partitionId); } catch (Exception e) { logger.warn("Failed to receive messages: {}", e.getMessage()); } } } } }