Add ROADM application

Change-Id: I50fa93cf3a69122f6434b46e831b254771159294
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/ChannelData.java b/apps/roadm/src/main/java/org/onosproject/roadm/ChannelData.java
new file mode 100644
index 0000000..07c6c41
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/ChannelData.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.OchSignalCriterion;
+import org.onosproject.net.flow.criteria.PortCriterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+
+/**
+ * Representation of an internal ROADM connection.
+ */
+public final class ChannelData {
+    private PortNumber inPort;
+    private PortNumber outPort;
+    private OchSignal ochSignal;
+
+    private ChannelData(PortNumber inPort, PortNumber outPort, OchSignal ochSignal) {
+        this.inPort = inPort;
+        this.outPort = outPort;
+        this.ochSignal = ochSignal;
+    }
+
+    /**
+     * Returns a ChannelData representation from a flow rule. The rule must contain
+     * a Criterion.Type.IN_PORT selector, Criterion.Type.OCH_SIGID selector, and
+     * Instruction.Type.OUTPUT instruction.
+     *
+     * @param rule the flow rule representing the connection
+     * @return ChannelData representation of the connection
+     */
+    public static ChannelData fromFlow(FlowRule rule) {
+        checkNotNull(rule);
+
+        Criterion in = rule.selector().getCriterion(Criterion.Type.IN_PORT);
+        checkNotNull(in);
+        PortNumber inPort = ((PortCriterion) in).port();
+
+        Criterion och = rule.selector().getCriterion(Criterion.Type.OCH_SIGID);
+        checkNotNull(och);
+        OchSignal ochSignal = ((OchSignalCriterion) och).lambda();
+
+        PortNumber outPort = null;
+        List<Instruction> instructions = rule.treatment().allInstructions();
+        for (Instruction ins : instructions) {
+            if (ins.type() == Instruction.Type.OUTPUT) {
+                outPort = ((Instructions.OutputInstruction) ins).port();
+            }
+        }
+        checkNotNull(outPort);
+
+        return new ChannelData(inPort, outPort, ochSignal);
+    }
+
+    /**
+     * Returns the input port.
+     *
+     * @return input port
+     */
+    public PortNumber inPort() {
+        return inPort;
+    }
+
+    /**
+     * Returns the output port.
+     *
+     * @return output port
+     */
+    public PortNumber outPort() {
+        return outPort;
+    }
+
+    /**
+     * Returns the channel signal.
+     *
+     * @return channel signal
+     */
+    public OchSignal ochSignal() {
+        return ochSignal;
+    }
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/DistributedRoadmStore.java b/apps/roadm/src/main/java/org/onosproject/roadm/DistributedRoadmStore.java
new file mode 100644
index 0000000..597ac2e
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/DistributedRoadmStore.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages the port target powers for ROADM devices.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedRoadmStore implements RoadmStore {
+    private static Logger log = LoggerFactory.getLogger(DistributedRoadmStore.class);
+
+    private ConsistentMap<DeviceId, Map<PortNumber, Long>> distPowerMap;
+    private Map<DeviceId, Map<PortNumber, Long>> powerMap;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    @Activate
+    public void activate() {
+        distPowerMap = storageService.<DeviceId, Map<PortNumber, Long>>consistentMapBuilder()
+                .withName("onos-roadm-distributed-store")
+                .withSerializer(Serializer.using(KryoNamespaces.API))
+                .build();
+        powerMap = distPowerMap.asJavaMap();
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    // Add a map to the store for a device if not already added.
+    // Powers still need to be initialized with calls to setTargetPower().
+    @Override
+    public void addDevice(DeviceId deviceId) {
+        powerMap.putIfAbsent(deviceId, new HashMap<>());
+        log.info("Initializing {}", deviceId);
+    }
+
+    // Returns true if Map for device exists in ConsistentMap
+    @Override
+    public boolean deviceAvailable(DeviceId deviceId) {
+        return powerMap.get(deviceId) != null;
+    }
+
+
+    @Override
+    public void setTargetPower(DeviceId deviceId, PortNumber portNumber, long targetPower) {
+        Map<PortNumber, Long> portMap = powerMap.get(deviceId);
+        if (portMap != null) {
+            portMap.put(portNumber, targetPower);
+            powerMap.put(deviceId, portMap);
+        } else {
+            log.info("Device {} not found in store", deviceId);
+        }
+    }
+
+    @Override
+    public Long getTargetPower(DeviceId deviceId, PortNumber portNumber) {
+        Map<PortNumber, Long> portMap = powerMap.get(deviceId);
+        if (portMap != null) {
+            return portMap.get(portNumber);
+        }
+        return null;
+    }
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmComponent.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmComponent.java
new file mode 100644
index 0000000..ffb332d
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmComponent.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiView;
+import org.onosproject.ui.UiViewHidden;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * ONOS UI for ROADM application.
+ */
+@Component(immediate = true)
+public class RoadmComponent {
+
+    private static final String DEVICE_VIEW_ID = "roadmDevice";
+    private static final String DEVICE_VIEW_TEXT = "ROADM";
+
+    private static final String RESOURCE_PATH = "webgui";
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected UiExtensionService uiExtensionService;
+
+    // List of application views
+    private final List<UiView> deviceViews = ImmutableList.of(
+            new UiView(UiView.Category.OTHER, DEVICE_VIEW_ID, DEVICE_VIEW_TEXT),
+            new UiViewHidden("roadmPort"),
+            new UiViewHidden("roadmFlow")
+    );
+
+    // Factory for UI message handlers
+    private final UiMessageHandlerFactory messageHandlerFactory =
+            () -> ImmutableList.of(
+                    new RoadmDeviceViewMessageHandler(),
+                    new RoadmPortViewMessageHandler(),
+                    new RoadmFlowViewMessageHandler()
+            );
+
+    // Device UI extension
+    protected UiExtension deviceExtension =
+            new UiExtension.Builder(getClass().getClassLoader(), deviceViews)
+                    .resourcePath(RESOURCE_PATH)
+                    .messageHandlerFactory(messageHandlerFactory)
+                    .build();
+
+    @Activate
+    protected void activate() {
+        uiExtensionService.register(deviceExtension);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        uiExtensionService.unregister(deviceExtension);
+        log.info("Stopped");
+    }
+
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmDeviceViewMessageHandler.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmDeviceViewMessageHandler.java
new file mode 100644
index 0000000..d2a6fe2
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmDeviceViewMessageHandler.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.Device;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.table.TableModel;
+import org.onosproject.ui.table.TableRequestHandler;
+
+import java.util.Collection;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Table-View message handler for ROADM device view.
+ */
+public class RoadmDeviceViewMessageHandler extends UiMessageHandler {
+
+    private static final String ROADM_DEVICE_DATA_REQ = "roadmDeviceDataRequest";
+    private static final String ROADM_DEVICE_DATA_RESP = "roadmDeviceDataResponse";
+    private static final String ROADM_DEVICES = "roadmDevices";
+
+    private static final String NO_ROWS_MESSAGE = "No items found";
+
+    private static final String ID = "id";
+    private static final String FRIENDLY_NAME = "name";
+    private static final String MASTER = "master";
+    private static final String PORTS = "ports";
+    private static final String VENDOR = "vendor";
+    private static final String HW_VERSION = "hwVersion";
+    private static final String SW_VERSION = "swVersion";
+    private static final String PROTOCOL = "protocol";
+
+    private static final String[] COLUMN_IDS = {
+            ID, FRIENDLY_NAME, MASTER, PORTS, VENDOR, HW_VERSION, SW_VERSION,
+            PROTOCOL
+    };
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(
+                new DeviceTableDataRequestHandler()
+        );
+    }
+
+    // Returns friendly name of the device from the annotations
+    private static String deviceName(Device device) {
+        String name = device.annotations().value(AnnotationKeys.NAME);
+        return isNullOrEmpty(name) ? device.id().toString() : name;
+    }
+
+    // Returns the device protocol from annotations
+    private static String deviceProtocol(Device device) {
+        String protocol = device.annotations().value(PROTOCOL);
+        return protocol != null ? protocol : "N/A";
+    }
+
+    // Handler for sample table requests
+    private final class DeviceTableDataRequestHandler extends TableRequestHandler {
+
+        private DeviceTableDataRequestHandler() {
+            super(ROADM_DEVICE_DATA_REQ, ROADM_DEVICE_DATA_RESP, ROADM_DEVICES);
+        }
+
+        @Override
+        protected String[] getColumnIds() {
+            return COLUMN_IDS;
+        }
+
+        @Override
+        protected String noRowsMessage(ObjectNode payload) {
+            return NO_ROWS_MESSAGE;
+        }
+
+        @Override
+        protected void populateTable(TableModel tm, ObjectNode payload) {
+            DeviceService ds = get(DeviceService.class);
+            MastershipService ms = get(MastershipService.class);
+            for (Device device : ds.getDevices(Device.Type.ROADM)) {
+                populateRow(tm.addRow(), device, ds, ms);
+            }
+        }
+
+        private void populateRow(TableModel.Row row, Device device, DeviceService ds,
+                MastershipService ms) {
+            row.cell(ID, device.id().toString())
+                    .cell(FRIENDLY_NAME, deviceName(device))
+                    .cell(MASTER, ms.getMasterFor(device.id()))
+                    .cell(PORTS, ds.getPorts(device.id()).size())
+                    .cell(VENDOR, device.manufacturer())
+                    .cell(HW_VERSION, device.hwVersion())
+                    .cell(SW_VERSION, device.swVersion())
+                    .cell(PROTOCOL, deviceProtocol(device));
+        }
+    }
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmFlowViewMessageHandler.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmFlowViewMessageHandler.java
new file mode 100644
index 0000000..08e74f0
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmFlowViewMessageHandler.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
+import org.onlab.osgi.ServiceDirectory;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowId;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiConnection;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.table.TableModel;
+import org.onosproject.ui.table.TableRequestHandler;
+import org.onosproject.ui.table.cell.HexLongFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Set;
+
+import static org.onosproject.ui.JsonUtils.node;
+import static org.onosproject.ui.JsonUtils.number;
+
+/**
+ * Table-View message handler for ROADM flow view.
+ */
+public class RoadmFlowViewMessageHandler extends UiMessageHandler {
+
+    private static final String ROADM_FLOW_DATA_REQ = "roadmFlowDataRequest";
+    private static final String ROADM_FLOW_DATA_RESP = "roadmFlowDataResponse";
+    private static final String ROADM_FLOWS = "roadmFlows";
+
+    private static final String ROADM_SET_ATTENUATION_REQ = "roadmSetAttenuationRequest";
+    private static final String ROADM_SET_ATTENUATION_RESP = "roadmSetAttenuationResponse";
+
+    private static final String ROADM_DELETE_FLOW_REQ = "roadmDeleteFlowRequest";
+
+    private static final String ROADM_CREATE_FLOW_REQ = "roadmCreateFlowRequest";
+    private static final String ROADM_CREATE_FLOW_RESP = "roadmCreateFlowResponse";
+
+    private static final String NO_ROWS_MESSAGE = "No items found";
+
+    private static final String DEV_ID = "devId";
+
+    private static final String ID = "id";
+    private static final String FLOW_ID = "flowId";
+    private static final String APP_ID = "appId";
+    private static final String GROUP_ID = "groupId";
+    private static final String TABLE_ID = "tableId";
+    private static final String PRIORITY = "priority";
+    private static final String PERMANENT = "permanent";
+    private static final String TIMEOUT = "timeout";
+    private static final String STATE = "state";
+    private static final String IN_PORT = "inPort";
+    private static final String OUT_PORT = "outPort";
+    private static final String CHANNEL_SPACING = "spacing";
+    private static final String CHANNEL_MULTIPLIER = "multiplier";
+    private static final String CURRENT_POWER = "currentPower";
+    private static final String ATTENUATION = "attenuation";
+    private static final String HAS_ATTENUATION = "hasAttenuation";
+
+    private static final String[] COLUMN_IDS = {
+            ID, FLOW_ID, APP_ID, GROUP_ID, TABLE_ID, PRIORITY, TIMEOUT,
+            PERMANENT, STATE, IN_PORT, OUT_PORT, CHANNEL_SPACING,
+            CHANNEL_MULTIPLIER, CURRENT_POWER, ATTENUATION, HAS_ATTENUATION
+    };
+
+    private static final String NA = "N/A";
+    private static final String UNKNOWN = "Unknown";
+
+    private static final long GHZ = 1_000_000_000L;
+
+    private FlowRuleService flowRuleService;
+    private RoadmService roadmService;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Override
+    public void init(UiConnection connection, ServiceDirectory directory) {
+        super.init(connection, directory);
+        flowRuleService = get(FlowRuleService.class);
+        roadmService = get(RoadmService.class);
+    }
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(
+                new FlowTableDataRequestHandler(),
+                new SetAttenuationRequestHandler(),
+                new DeleteConnectionRequestHandler(),
+                new CreateConnectionRequestHandler()
+        );
+    }
+
+    // Handler for sample table requests
+    private final class FlowTableDataRequestHandler extends TableRequestHandler {
+
+        private FlowTableDataRequestHandler() {
+            super(ROADM_FLOW_DATA_REQ, ROADM_FLOW_DATA_RESP, ROADM_FLOWS);
+        }
+
+        @Override
+        protected String[] getColumnIds() {
+            return COLUMN_IDS;
+        }
+
+        @Override
+        protected String noRowsMessage(ObjectNode payload) {
+            return NO_ROWS_MESSAGE;
+        }
+
+        @Override
+        protected TableModel createTableModel() {
+            TableModel tm = super.createTableModel();
+            tm.setFormatter(FLOW_ID, HexLongFormatter.INSTANCE);
+            return tm;
+        }
+
+        @Override
+        protected void populateTable(TableModel tm, ObjectNode payload) {
+            DeviceId deviceId = DeviceId.deviceId(string(payload, DEV_ID, "(none)"));
+
+            Iterable<FlowEntry> flowEntries = flowRuleService.getFlowEntries(deviceId);
+            for (FlowEntry flowEntry : flowEntries) {
+                populateRow(tm.addRow(), flowEntry, deviceId);
+            }
+        }
+
+        private void populateRow(TableModel.Row row, FlowEntry entry, DeviceId deviceId) {
+            ChannelData cd = ChannelData.fromFlow(entry);
+            row.cell(ID, entry.id().value())
+                    .cell(FLOW_ID, entry.id().value())
+                    .cell(APP_ID, entry.appId())
+                    .cell(PRIORITY, entry.priority())
+                    .cell(TIMEOUT, entry.timeout())
+                    .cell(PERMANENT, entry.isPermanent())
+                    .cell(STATE, entry.state().toString())
+                    .cell(IN_PORT, cd.inPort().toLong())
+                    .cell(OUT_PORT, cd.outPort().toLong())
+                    .cell(CHANNEL_SPACING, cd.ochSignal().channelSpacing().frequency().asHz() / GHZ)
+                    .cell(CHANNEL_MULTIPLIER, cd.ochSignal().spacingMultiplier())
+                    .cell(CURRENT_POWER, getCurrentPower(deviceId, cd))
+                    .cell(ATTENUATION, getAttenuation(deviceId, cd));
+        }
+
+        private String getCurrentPower(DeviceId deviceId, ChannelData channelData) {
+            Range<Long> range =
+                    roadmService.attenuationRange(deviceId,
+                                                  channelData.outPort(),
+                                                  channelData.ochSignal());
+            if (range != null) {
+                Long currentPower =
+                        roadmService.getCurrentChannelPower(deviceId,
+                                                            channelData.outPort(),
+                                                            channelData.ochSignal());
+                if (currentPower != null) {
+                    return String.valueOf(currentPower);
+                }
+            }
+            return NA;
+        }
+
+        private String getAttenuation(DeviceId deviceId, ChannelData channelData) {
+            Long attenuation =
+                    roadmService.getAttenuation(deviceId, channelData.outPort(),
+                                                channelData.ochSignal());
+            if (attenuation != null) {
+                return String.valueOf(attenuation);
+            }
+            return UNKNOWN;
+        }
+    }
+
+    // Handler for setting attenuation
+    private final class SetAttenuationRequestHandler extends RequestHandler {
+
+        // Keys for response message
+        private static final String VALID = "valid";
+        private static final String MESSAGE = "message";
+
+        // Error messages to display to user
+        private static final String ATTENUATION_RANGE_MSG =
+                "Attenuation must be in range %s.";
+        private static final String NO_ATTENUATION_MSG =
+                "Cannot set attenuation for this connection";
+
+        private SetAttenuationRequestHandler() {
+            super(ROADM_SET_ATTENUATION_REQ);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            DeviceId deviceId = DeviceId.deviceId(string(payload, DEV_ID, "(none)"));
+            FlowId flowId = FlowId.valueOf(number(payload, FLOW_ID));
+            long attenuation = payload.get(ATTENUATION).asLong();
+
+            // Get connection information from the flow
+            FlowEntry entry = findFlow(deviceId, flowId);
+            if (entry == null) {
+                log.error("Unable to find flow rule to set attenuation");
+                return;
+            }
+            ChannelData cd = ChannelData.fromFlow(entry);
+            Range<Long> range =
+                    roadmService.attenuationRange(deviceId, cd.outPort(),
+                                                  cd.ochSignal());
+
+            boolean validAttenuation = (range != null && range.contains(attenuation));
+            if (validAttenuation) {
+                roadmService.setAttenuation(deviceId, cd.outPort(),
+                                            cd.ochSignal(), attenuation);
+            }
+
+            ObjectNode rootNode = objectNode();
+            // Send back flowId so view can identify which callback function to use
+            rootNode.put(FLOW_ID, payload.get(FLOW_ID).asText());
+            rootNode.put(VALID, validAttenuation);
+            if (range != null) {
+                rootNode.put(MESSAGE, String.format(ATTENUATION_RANGE_MSG,
+                                                    range.toString()));
+            } else {
+                rootNode.put(MESSAGE, NO_ATTENUATION_MSG);
+            }
+            sendMessage(ROADM_SET_ATTENUATION_RESP, rootNode);
+        }
+
+        private FlowEntry findFlow(DeviceId deviceId, FlowId flowId) {
+            for (FlowEntry entry : flowRuleService.getFlowEntries(deviceId)) {
+                if (entry.id().equals(flowId)) {
+                    return entry;
+                }
+            }
+            return null;
+        }
+    }
+
+    // Handler for deleting a connection
+    private final class DeleteConnectionRequestHandler extends RequestHandler {
+        private DeleteConnectionRequestHandler() {
+            super(ROADM_DELETE_FLOW_REQ);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            DeviceId deviceId = DeviceId.deviceId(string(payload, DEV_ID, "(none)"));
+            FlowId flowId = FlowId.valueOf(payload.get(ID).asLong());
+            roadmService.removeConnection(deviceId, flowId);
+        }
+    }
+
+    // Handler for creating a creating a connection from form data
+    private final class CreateConnectionRequestHandler extends RequestHandler {
+
+        // Keys to load from JSON
+        private static final String FORM_DATA = "formData";
+        private static final String CHANNEL_SPACING_INDEX = "index";
+        private static final String INCLUDE_ATTENUATION = "includeAttenuation";
+
+        // Keys for validation results
+        private static final String CONNECTION = "connection";
+        private static final String CHANNEL_AVAILABLE = "channelAvailable";
+
+        // Error messages to display to user
+        private static final String IN_PORT_ERR_MSG = "Invalid input port.";
+        private static final String OUT_PORT_ERR_MSG = "Invalid output port.";
+        private static final String CONNECTION_ERR_MSG =
+                "Invalid connection from input port to output port.";
+        private static final String CHANNEL_SPACING_ERR_MSG =
+                "Channel spacing not supported.";
+        private static final String CHANNEL_ERR_MSG =
+                "Channel index must be in range %s.";
+        private static final String CHANNEL_AVAILABLE_ERR_MSG =
+                "Channel is already being used.";
+        private static final String ATTENUATION_ERR_MSG =
+                "Attenuation must be in range %s.";
+
+        // Keys for validation object
+        private static final String VALID = "valid";
+        private static final String MESSAGE = "message";
+
+        private CreateConnectionRequestHandler() {
+            super(ROADM_CREATE_FLOW_REQ);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            DeviceId did = DeviceId.deviceId(string(payload, DEV_ID, "(none)"));
+            ObjectNode flowNode = node(payload, FORM_DATA);
+            int priority = (int) number(flowNode, PRIORITY);
+            boolean permanent = bool(flowNode, PERMANENT);
+            int timeout = (int) number(flowNode, TIMEOUT);
+            PortNumber inPort = PortNumber.portNumber(number(flowNode, IN_PORT));
+            PortNumber outPort = PortNumber.portNumber(number(flowNode, OUT_PORT));
+            ObjectNode chNode = node(flowNode, CHANNEL_SPACING);
+            ChannelSpacing spacing =
+                    channelSpacing((int) number(chNode, CHANNEL_SPACING_INDEX));
+            int multiplier = (int) number(flowNode, CHANNEL_MULTIPLIER);
+            OchSignal och = OchSignal.newDwdmSlot(spacing, multiplier);
+            boolean includeAttenuation = bool(flowNode, INCLUDE_ATTENUATION);
+            long att = number(flowNode, ATTENUATION);
+
+            boolean validInPort = roadmService.validInputPort(did, inPort);
+            boolean validOutPort = roadmService.validOutputPort(did, outPort);
+            boolean validConnect = roadmService.validConnection(did, inPort, outPort);
+            boolean validSpacing = true;
+            boolean validChannel = roadmService.validChannel(did, inPort, och);
+            boolean channelAvailable = roadmService.channelAvailable(did, och);
+            boolean validAttenuation = roadmService.attenuationInRange(did, outPort, att);
+
+            if (validConnect && validChannel && channelAvailable) {
+                if (includeAttenuation && validAttenuation) {
+                    roadmService.createConnection(did, priority, permanent,
+                                                  timeout, inPort, outPort,
+                                                  och, att);
+                } else if (!includeAttenuation) {
+                    roadmService.createConnection(did, priority, permanent,
+                                                  timeout, inPort, outPort,
+                                                  och);
+                }
+            }
+
+            // Construct error for channel
+            String channelMessage = "Invalid channel";
+            if (!validChannel) {
+                Set<OchSignal> lambdas = roadmService.queryLambdas(did, outPort);
+                if (lambdas != null) {
+                    Range<Integer> range = channelRange(lambdas);
+                    if (range.contains(och.spacingMultiplier())) {
+                        // Channel spacing error
+                        validSpacing = false;
+                    } else {
+                        channelMessage = String.format(CHANNEL_ERR_MSG, range.toString());
+                    }
+                }
+            }
+
+            // Construct error for attenuation
+            String attenuationMessage = "Invalid attenuation";
+            if (!validAttenuation) {
+                Range<Long> range =
+                        roadmService.attenuationRange(did, outPort, och);
+                if (range != null) {
+                    attenuationMessage =
+                            String.format(ATTENUATION_ERR_MSG, range.toString());
+                }
+            }
+
+            // Build response
+            ObjectNode node = objectNode();
+
+            node.set(IN_PORT, validationObject(validInPort, IN_PORT_ERR_MSG));
+            node.set(OUT_PORT, validationObject(validOutPort, OUT_PORT_ERR_MSG));
+            node.set(CONNECTION, validationObject(validConnect, CONNECTION_ERR_MSG));
+            node.set(CHANNEL_SPACING, validationObject(validChannel || validSpacing,
+                                                       CHANNEL_SPACING_ERR_MSG));
+            node.set(CHANNEL_MULTIPLIER, validationObject(validChannel || !validSpacing,
+                                                          channelMessage));
+            node.set(CHANNEL_AVAILABLE, validationObject(!validChannel || channelAvailable,
+                                                         CHANNEL_AVAILABLE_ERR_MSG));
+            node.set(ATTENUATION, validationObject(validAttenuation, attenuationMessage));
+            node.put(INCLUDE_ATTENUATION, includeAttenuation);
+
+            sendMessage(ROADM_CREATE_FLOW_RESP, node);
+        }
+
+        // Returns the ChannelSpacing based on the selection made
+        private ChannelSpacing channelSpacing(int selectionIndex) {
+            switch (selectionIndex) {
+                case 0: return ChannelSpacing.CHL_100GHZ;
+                case 1: return ChannelSpacing.CHL_50GHZ;
+                case 2: return ChannelSpacing.CHL_25GHZ;
+                case 3: return ChannelSpacing.CHL_12P5GHZ;
+                // 6.25GHz cannot be used with ChannelSpacing.newDwdmSlot
+                // case 4: return ChannelSpacing.CHL_6P25GHZ;
+                default: return ChannelSpacing.CHL_50GHZ;
+            }
+        }
+
+        // Construct validation object to return to the view
+        private ObjectNode validationObject(boolean result, String message) {
+            ObjectNode node = objectNode();
+            node.put(VALID, result);
+            if (!result) {
+                // return error message to display if validation failed
+                node.put(MESSAGE, message);
+            }
+            return node;
+        }
+
+        // Returns the minimum and maximum channel spacing
+        private Range<Integer> channelRange(Set<OchSignal> signals) {
+            Comparator<OchSignal> compare =
+                    (OchSignal a, OchSignal b) -> a.spacingMultiplier() - b.spacingMultiplier();
+            OchSignal minOch = Collections.min(signals, compare);
+            OchSignal maxOch = Collections.max(signals, compare);
+            return Range.closed(minOch.spacingMultiplier(), maxOch.spacingMultiplier());
+        }
+    }
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmManager.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmManager.java
new file mode 100644
index 0000000..011c393
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmManager.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import com.google.common.collect.Range;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Direction;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.OchSignalType;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.LambdaQuery;
+import org.onosproject.net.behaviour.PowerConfig;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowId;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Application for monitoring and configuring ROADM devices.
+ */
+@Component(immediate = true)
+@Service
+public class RoadmManager implements RoadmService {
+
+    private static final String APP_NAME = "org.onosproject.roadm";
+    private ApplicationId appId;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private DeviceListener deviceListener = new InternalDeviceListener();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected RoadmStore roadmStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowRuleService flowRuleService;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(APP_NAME);
+        deviceService.addListener(deviceListener);
+        initDevices();
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        deviceService.removeListener(deviceListener);
+
+        log.info("Stopped");
+    }
+
+    private PowerConfig<Object> getPowerConfig(DeviceId deviceId) {
+        Device device = deviceService.getDevice(deviceId);
+        if (device != null && device.is(PowerConfig.class)) {
+            return device.as(PowerConfig.class);
+        }
+        log.warn("Unable to load PowerConfig for {}", deviceId);
+        return null;
+    }
+
+    private LambdaQuery getLambdaQuery(DeviceId deviceId) {
+        Device device = deviceService.getDevice(deviceId);
+        if (device != null && device.is(LambdaQuery.class)) {
+            return device.as(LambdaQuery.class);
+        }
+        return null;
+    }
+
+    private void initDevices() {
+        for (Device device : deviceService.getDevices(Device.Type.ROADM)) {
+            initDevice(device.id());
+            setAllInitialTargetPortPowers(device.id());
+        }
+    }
+
+    // Initialize RoadmStore for a device to support target power
+    private void initDevice(DeviceId deviceId) {
+        if (!roadmStore.deviceAvailable(deviceId)) {
+            roadmStore.addDevice(deviceId);
+        }
+        log.info("Initialized device {}", deviceId);
+    }
+
+    // Sets the target port powers for a port on a device
+    // Attempts to read target powers from store. If no value is found then
+    // default value is used instead.
+    private void setInitialTargetPortPower(DeviceId deviceId, PortNumber portNumber) {
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig == null) {
+            log.warn("Unable to set default initial powers for port {} on device {}",
+                     portNumber, deviceId);
+            return;
+        }
+
+        Optional<Range<Long>> range =
+                powerConfig.getTargetPowerRange(portNumber, Direction.ALL);
+        if (!range.isPresent()) {
+            log.warn("No target power range found for port {} on device {}",
+                     portNumber, deviceId);
+            return;
+        }
+
+        Long power = roadmStore.getTargetPower(deviceId, portNumber);
+        if (power == null) {
+            // Set default to middle of the range
+            power = (range.get().lowerEndpoint() + range.get().upperEndpoint()) / 2;
+            roadmStore.setTargetPower(deviceId, portNumber, power);
+        }
+        powerConfig.setTargetPower(portNumber, Direction.ALL, power);
+    }
+
+    // Sets the target port powers for each each port on a device
+    // Attempts to read target powers from store. If no value is found then
+    // default value is used instead
+    private void setAllInitialTargetPortPowers(DeviceId deviceId) {
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig == null) {
+            log.warn("Unable to set default initial powers for device {}",
+                     deviceId);
+            return;
+        }
+
+        List<Port> ports = deviceService.getPorts(deviceId);
+        for (Port port : ports) {
+            Optional<Range<Long>> range =
+                    powerConfig.getTargetPowerRange(port.number(), Direction.ALL);
+            if (range.isPresent()) {
+                Long power = roadmStore.getTargetPower(deviceId, port.number());
+                if (power == null) {
+                    // Set default to middle of the range
+                    power = (range.get().lowerEndpoint() + range.get().upperEndpoint()) / 2;
+                    roadmStore.setTargetPower(deviceId, port.number(), power);
+                }
+                powerConfig.setTargetPower(port.number(), Direction.ALL, power);
+            } else {
+                log.warn("No target power range found for port {} on device {}",
+                         port.number(), deviceId);
+            }
+        }
+    }
+
+    @Override
+    public void setTargetPortPower(DeviceId deviceId, PortNumber portNumber, long power) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            roadmStore.setTargetPower(deviceId, portNumber, power);
+            powerConfig.setTargetPower(portNumber, Direction.ALL, power);
+        } else {
+            log.warn("Unable to set target port power for device {}", deviceId);
+        }
+    }
+
+    @Override
+    public Long getTargetPortPower(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        // getTargetPortPower is not yet implemented in PowerConfig so we
+        // access store instead
+        return roadmStore.getTargetPower(deviceId, portNumber);
+    }
+
+    @Override
+    public void setAttenuation(DeviceId deviceId, PortNumber portNumber,
+                               OchSignal ochSignal, long attenuation) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        checkNotNull(ochSignal);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            powerConfig.setTargetPower(portNumber, ochSignal, attenuation);
+        } else {
+            log.warn("Cannot set attenuation for channel index {} on device {}",
+                     ochSignal.spacingMultiplier(), deviceId);
+        }
+    }
+
+    @Override
+    public Long getAttenuation(DeviceId deviceId, PortNumber portNumber,
+                               OchSignal ochSignal) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        checkNotNull(ochSignal);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Long> attenuation =
+                    powerConfig.getTargetPower(portNumber, ochSignal);
+            if (attenuation.isPresent()) {
+                return attenuation.get();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Long getCurrentPortPower(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Long> currentPower =
+                    powerConfig.currentPower(portNumber, Direction.ALL);
+            if (currentPower.isPresent()) {
+                return currentPower.get();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Long getCurrentChannelPower(DeviceId deviceId, PortNumber portNumber,
+                                       OchSignal ochSignal) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        checkNotNull(ochSignal);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Long> currentPower =
+                    powerConfig.currentPower(portNumber, ochSignal);
+            if (currentPower.isPresent()) {
+                return currentPower.get();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Set<OchSignal> queryLambdas(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        LambdaQuery lambdaQuery = getLambdaQuery(deviceId);
+        if (lambdaQuery != null) {
+            return lambdaQuery.queryLambdas(portNumber);
+        }
+        return Collections.emptySet();
+    }
+
+    @Override
+    public FlowId createConnection(DeviceId deviceId, int priority, boolean isPermanent,
+                                 int timeout, PortNumber inPort, PortNumber outPort,
+                                 OchSignal ochSignal) {
+        checkNotNull(deviceId);
+        checkNotNull(inPort);
+        checkNotNull(outPort);
+
+        FlowRule.Builder flowBuilder = new DefaultFlowRule.Builder();
+        flowBuilder.fromApp(appId);
+        flowBuilder.withPriority(priority);
+        if (isPermanent) {
+            flowBuilder.makePermanent();
+        } else {
+            flowBuilder.makeTemporary(timeout);
+        }
+        flowBuilder.forDevice(deviceId);
+
+        TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector.builder();
+        selectorBuilder.add(Criteria.matchInPort(inPort));
+        selectorBuilder.add(Criteria.matchOchSignalType(OchSignalType.FIXED_GRID));
+        selectorBuilder.add(Criteria.matchLambda(ochSignal));
+        flowBuilder.withSelector(selectorBuilder.build());
+
+        TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment.builder();
+        treatmentBuilder.add(Instructions.createOutput(outPort));
+        flowBuilder.withTreatment(treatmentBuilder.build());
+
+        FlowRule flowRule = flowBuilder.build();
+        flowRuleService.applyFlowRules(flowRule);
+
+        log.info("Created connection from input port {} to output port {}",
+                 inPort.toLong(), outPort.toLong());
+
+        return flowRule.id();
+    }
+
+    @Override
+    public FlowId createConnection(DeviceId deviceId, int priority, boolean isPermanent,
+                                 int timeout, PortNumber inPort, PortNumber outPort,
+                                 OchSignal ochSignal, long attenuation) {
+        checkNotNull(deviceId);
+        checkNotNull(inPort);
+        checkNotNull(outPort);
+        FlowId flowId = createConnection(deviceId, priority, isPermanent,
+                                         timeout, inPort, outPort, ochSignal);
+        delayedSetAttenuation(deviceId, outPort, ochSignal, attenuation);
+        return flowId;
+    }
+
+    // Delay the call to setTargetPower because the flow may not be in the store yet
+    private void delayedSetAttenuation(DeviceId deviceId, PortNumber outPort,
+                                       OchSignal ochSignal, long attenuation) {
+        Runnable setAtt = () -> {
+            try {
+                TimeUnit.SECONDS.sleep(1);
+            } catch (InterruptedException e) {
+                log.warn("Thread interrupted. Setting attenuation early.");
+            }
+            setAttenuation(deviceId, outPort, ochSignal, attenuation);
+        };
+        new Thread(setAtt).start();
+    }
+
+    @Override
+    public void removeConnection(DeviceId deviceId, FlowId flowId) {
+        checkNotNull(deviceId);
+        checkNotNull(flowId);
+        for (FlowEntry entry : flowRuleService.getFlowEntries(deviceId)) {
+            if (entry.id().equals(flowId)) {
+                flowRuleService.removeFlowRules(entry);
+                log.info("Deleted connection {}", entry.id());
+                break;
+            }
+        }
+    }
+
+    @Override
+    public boolean hasPortTargetPower(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Range<Long>> range =
+                    powerConfig.getTargetPowerRange(portNumber, Direction.ALL);
+            return range.isPresent();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean portTargetPowerInRange(DeviceId deviceId, PortNumber portNumber,
+                                          long power) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Range<Long>> range =
+                    powerConfig.getTargetPowerRange(portNumber, Direction.ALL);
+            return range.isPresent() && range.get().contains(power);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean attenuationInRange(DeviceId deviceId, PortNumber outPort,
+                                      long att) {
+        checkNotNull(deviceId);
+        checkNotNull(outPort);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            OchSignal stubOch = OchSignal.newDwdmSlot(ChannelSpacing.CHL_50GHZ, 0);
+            Optional<Range<Long>> range =
+                    powerConfig.getTargetPowerRange(outPort, stubOch);
+            return range.isPresent() && range.get().contains(att);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean validInputPort(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Range<Long>> range =
+                    powerConfig.getInputPowerRange(portNumber, Direction.ALL);
+            return range.isPresent();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean validOutputPort(DeviceId deviceId, PortNumber portNumber) {
+        return hasPortTargetPower(deviceId, portNumber);
+    }
+
+    @Override
+    public boolean validChannel(DeviceId deviceId, PortNumber portNumber,
+                                OchSignal ochSignal) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        LambdaQuery lambdaQuery = getLambdaQuery(deviceId);
+        if (lambdaQuery != null) {
+            Set<OchSignal> channels = lambdaQuery.queryLambdas(portNumber);
+            return channels.contains(ochSignal);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean channelAvailable(DeviceId deviceId, OchSignal ochSignal) {
+        checkNotNull(deviceId);
+        checkNotNull(ochSignal);
+        for (FlowEntry entry : flowRuleService.getFlowEntries(deviceId)) {
+            if (ChannelData.fromFlow(entry).ochSignal().equals(ochSignal)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean validConnection(DeviceId deviceId, PortNumber inPort,
+                                   PortNumber outPort) {
+        checkNotNull(deviceId);
+        checkNotNull(inPort);
+        checkNotNull(outPort);
+        return validInputPort(deviceId, inPort) && validOutputPort(deviceId, outPort);
+    }
+
+    @Override
+    public Range<Long> targetPortPowerRange(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Range<Long>> range =
+                    powerConfig.getTargetPowerRange(portNumber, Direction.ALL);
+            if (range.isPresent()) {
+                return range.get();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Range<Long> attenuationRange(DeviceId deviceId, PortNumber portNumber,
+                                        OchSignal ochSignal) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        checkNotNull(ochSignal);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Range<Long>> range =
+                    powerConfig.getTargetPowerRange(portNumber, ochSignal);
+            if (range.isPresent()) {
+                return range.get();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Range<Long> inputPortPowerRange(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId);
+        checkNotNull(portNumber);
+        PowerConfig<Object> powerConfig = getPowerConfig(deviceId);
+        if (powerConfig != null) {
+            Optional<Range<Long>> range =
+                    powerConfig.getInputPowerRange(portNumber, Direction.ALL);
+            if (range.isPresent()) {
+                return range.get();
+            }
+        }
+        return null;
+    }
+
+    // Listens to device events.
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent deviceEvent) {
+            Device device = deviceEvent.subject();
+
+            switch (deviceEvent.type()) {
+                case DEVICE_ADDED:
+                case DEVICE_UPDATED:
+                    initDevice(device.id());
+                    break;
+                case PORT_ADDED:
+                case PORT_UPDATED:
+                    setInitialTargetPortPower(device.id(), deviceEvent.port().number());
+                    break;
+                default:
+                    break;
+
+            }
+        }
+    }
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmPortViewMessageHandler.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmPortViewMessageHandler.java
new file mode 100644
index 0000000..81fb581
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmPortViewMessageHandler.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
+import org.onlab.osgi.ServiceDirectory;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.optical.OpticalAnnotations;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiConnection;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.table.TableModel;
+import org.onosproject.ui.table.TableRequestHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Table-View message handler for ROADM port view.
+ */
+public class RoadmPortViewMessageHandler extends UiMessageHandler {
+
+    private static final String ROADM_PORT_DATA_REQ = "roadmPortDataRequest";
+    private static final String ROADM_PORT_DATA_RESP = "roadmPortDataResponse";
+    private static final String ROADM_PORTS = "roadmPorts";
+
+    private static final String ROADM_SET_TARGET_POWER_REQ = "roadmSetTargetPowerRequest";
+    private static final String ROADM_SET_TARGET_POWER_RESP = "roadmSetTargetPowerResponse";
+
+    private static final String NO_ROWS_MESSAGE = "No items found";
+
+    private static final String DEV_ID = "devId";
+
+    private static final String ID = "id";
+    private static final String TYPE = "type";
+    private static final String NAME = "name";
+    private static final String ENABLED = "enabled";
+    private static final String MIN_FREQ = "minFreq";
+    private static final String MAX_FREQ = "maxFreq";
+    private static final String GRID = "grid";
+    private static final String INPUT_POWER_RANGE = "inputPowerRange";
+    private static final String CURRENT_POWER = "currentPower";
+    private static final String TARGET_POWER = "targetPower";
+    private static final String HAS_TARGET_POWER = "hasTargetPower";
+
+    private static final String[] COLUMN_IDS = {
+            ID, TYPE, NAME, ENABLED, MIN_FREQ, MAX_FREQ, GRID, INPUT_POWER_RANGE,
+            CURRENT_POWER, TARGET_POWER, HAS_TARGET_POWER,
+    };
+
+    private static final String NA = "N/A";
+    private static final String UNKNOWN = "Unknown";
+
+    private static final long GHZ = 1_000_000_000L;
+    private static final long THZ = 1_000_000_000_000L;
+
+    private DeviceService deviceService;
+    private RoadmService roadmService;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Override
+    public void init(UiConnection connection, ServiceDirectory directory) {
+        super.init(connection, directory);
+        deviceService = get(DeviceService.class);
+        roadmService = get(RoadmService.class);
+    }
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(
+                new PortTableDataRequestHandler(),
+                new SetTargetPowerRequestHandler()
+        );
+    }
+
+    private String asGHz(String value) {
+        return String.valueOf(Double.valueOf(value) / GHZ);
+    }
+
+    private String asTHz(String value) {
+        return String.valueOf(Double.valueOf(value) / THZ);
+    }
+
+    private String annotation(Port port, String key, String defaultValue) {
+        String value = port.annotations().value(key);
+        return value != null ? value : defaultValue;
+    }
+
+    private String annotation(Port port, String key) {
+        return annotation(port, key, NA);
+    }
+
+    // Handler for sample table requests
+    private final class PortTableDataRequestHandler extends TableRequestHandler {
+
+        private PortTableDataRequestHandler() {
+            super(ROADM_PORT_DATA_REQ, ROADM_PORT_DATA_RESP, ROADM_PORTS);
+        }
+
+        @Override
+        protected String[] getColumnIds() {
+            return COLUMN_IDS;
+        }
+
+        @Override
+        protected String noRowsMessage(ObjectNode payload) {
+            return NO_ROWS_MESSAGE;
+        }
+
+        @Override
+        protected void populateTable(TableModel tm, ObjectNode payload) {
+            DeviceId deviceId = DeviceId.deviceId(string(payload, DEV_ID, "(none)"));
+
+            if (deviceService.isAvailable(deviceId)) {
+                List<Port> ports = deviceService.getPorts(deviceId);
+                for (Port port : ports) {
+                    populateRow(tm.addRow(), port, deviceId);
+                }
+            }
+        }
+
+        private void populateRow(TableModel.Row row, Port port, DeviceId deviceId) {
+            row.cell(ID, port.number().toLong())
+                    .cell(TYPE, port.type())
+                    .cell(ENABLED, port.isEnabled())
+                    .cell(NAME, annotation(port, AnnotationKeys.PORT_NAME))
+                    .cell(MIN_FREQ, asTHz(annotation(port, OpticalAnnotations.MIN_FREQ_HZ)))
+                    .cell(MAX_FREQ, asTHz(annotation(port, OpticalAnnotations.MAX_FREQ_HZ)))
+                    .cell(GRID, asGHz(annotation(port, OpticalAnnotations.GRID_HZ)))
+                    .cell(INPUT_POWER_RANGE, getInputPowerRange(deviceId, port.number()))
+                    .cell(CURRENT_POWER, getCurrentPower(deviceId, port.number()))
+                    .cell(TARGET_POWER, getTargetPower(deviceId, port.number()))
+                    .cell(HAS_TARGET_POWER, roadmService.hasPortTargetPower(deviceId, port.number()));
+        }
+
+        // Returns the input power range as a string, N/A if the port is not an
+        // input port
+        private String getInputPowerRange(DeviceId deviceId, PortNumber portNumber) {
+            Range<Long> range =
+                    roadmService.inputPortPowerRange(deviceId, portNumber);
+            if (range != null) {
+                return range.toString();
+            }
+            return NA;
+        }
+
+        // Returns the current power as a string, Unknown if no value can be found.
+        private String getCurrentPower(DeviceId deviceId, PortNumber portNumber) {
+            Long currentPower =
+                    roadmService.getCurrentPortPower(deviceId, portNumber);
+            if (currentPower != null) {
+                return String.valueOf(currentPower);
+            }
+            return UNKNOWN;
+        }
+
+        // Returns target power as a string, Unknown if target power is expected but
+        // cannot be found, N/A if port does not have configurable target power
+        private String getTargetPower(DeviceId deviceId, PortNumber portNumber) {
+            if (roadmService.hasPortTargetPower(deviceId, portNumber)) {
+                Long targetPower =
+                        roadmService.getTargetPortPower(deviceId, portNumber);
+                if (targetPower != null) {
+                    return String.valueOf(targetPower);
+                } else {
+                    return UNKNOWN;
+                }
+            }
+            return NA;
+        }
+    }
+
+
+    // Handler for setting port target power
+    private final class SetTargetPowerRequestHandler extends RequestHandler {
+
+        private static final String VALID = "valid";
+        private static final String MESSAGE = "message";
+
+        private static final String TARGET_POWER_ERR_MSG = "Target power range is %s.";
+
+        private SetTargetPowerRequestHandler() {
+            super(ROADM_SET_TARGET_POWER_REQ);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            DeviceId deviceId = DeviceId.deviceId(string(payload, DEV_ID, "(none)"));
+            PortNumber portNumber = PortNumber.portNumber(payload.get(ID).asLong());
+            long targetPower = payload.get(TARGET_POWER).asLong();
+            boolean validTargetPower;
+
+            Range<Long> range =
+                    roadmService.targetPortPowerRange(deviceId, portNumber);
+            if (range != null) {
+                validTargetPower = range.contains(targetPower);
+
+                if (validTargetPower) {
+                    roadmService.setTargetPortPower(deviceId, portNumber, targetPower);
+                }
+
+                ObjectNode rootNode = objectNode();
+                rootNode.put(ID, payload.get(ID).asText());
+                rootNode.put(VALID, validTargetPower);
+                rootNode.put(MESSAGE, String.format(TARGET_POWER_ERR_MSG, range.toString()));
+                sendMessage(ROADM_SET_TARGET_POWER_RESP, rootNode);
+
+            } else {
+                log.warn("Unable to determine target power range for device {}", deviceId);
+            }
+        }
+    }
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmService.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmService.java
new file mode 100644
index 0000000..19334ae
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmService.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import com.google.common.collect.Range;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.FlowId;
+
+import java.util.Set;
+
+/**
+ * ROADM service interface. Provides an interface for ROADM power configuration.
+ *
+ * This application relies on the PowerConfig and LambdaQuery behaviours.
+ *
+ * The device's PowerConfig implementation should be parameterized as
+ * {@code PowerConfig<Object>} in order to support both Direction and OchSignal.
+ * For a reference implementation of PowerConfig, please see
+ * OplinkRoadmPowerConfig
+ *
+ * In this application, a "connection" refers to the selection of a channel
+ * to direct from an input to an output port. Connections are implemented
+ * using FlowRules with an input port selector, optical channel selector,
+ * and output port treatment (see RoadmManager#createConnection()).
+ *
+ * This application currently only supports fixed grid channels.
+ */
+public interface RoadmService {
+
+    /**
+     * Set target power for a port if the port has configurable target power.
+     *
+     * @param deviceId DeviceId of the device to configure
+     * @param portNumber PortNumber of the port to configure
+     * @param power value to set target power to
+     */
+    void setTargetPortPower(DeviceId deviceId, PortNumber portNumber, long power);
+
+    /**
+     * Returns the target power for a port if the port has configurable target power.
+     *
+     * @param deviceId DeviceId of the device to configure
+     * @param portNumber PortNumber of the port to configure
+     * @return the target power if the port has a target power, null otherwise
+     */
+    Long getTargetPortPower(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Sets the attenuation of a connection. This does not check that attenuation
+     * is within the acceptable range.
+     *
+     * @param deviceId DeviceId of the device to configure
+     * @param portNumber PortNumber of either the input or output port
+     * @param ochSignal channel to set attenuation for
+     * @param attenuation attenuation value to set to
+     */
+    void setAttenuation(DeviceId deviceId, PortNumber portNumber, OchSignal ochSignal,
+                        long attenuation);
+
+    /**
+     * Returns the attenuation of a connection.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of either the input or output port
+     * @param ochSignal channel to search for
+     * @return attenuation if found, null otherwise
+     */
+    Long getAttenuation(DeviceId deviceId, PortNumber portNumber, OchSignal ochSignal);
+
+    /**
+     * Returns the current port power.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of the port
+     * @return current power if found, null otherwise
+     */
+    Long getCurrentPortPower(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Returns the current channel power.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of either the input or output port of the connection
+     * @param ochSignal channel to search for
+     * @return channel power if found, null otherwise
+     */
+    Long getCurrentChannelPower(DeviceId deviceId, PortNumber portNumber,
+                                OchSignal ochSignal);
+
+    /**
+     * Returns the channels supported by a port.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of the port
+     * @return the set of supported channels
+     */
+    Set<OchSignal> queryLambdas(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Creates a new internal connection on a device without attenuation. This does
+     * not check that the connection is actually valid (e.g. an input port to an
+     * output port).
+     *
+     * Connections are represented as flows with an input port, output port, and
+     * channel. Implementation of attenuation is up to the vendor.
+     *
+     * @param deviceId DeviceId of the device to create this connection for
+     * @param priority priority of the flow
+     * @param isPermanent permanence of the flow
+     * @param timeout timeout in seconds
+     * @param inPort input port
+     * @param outPort output port
+     * @param ochSignal channel to use
+     * @return FlowId of the FlowRule representing the connection
+     */
+    FlowId createConnection(DeviceId deviceId, int priority, boolean isPermanent,
+                          int timeout, PortNumber inPort, PortNumber outPort,
+                          OchSignal ochSignal);
+
+    /**
+     * Creates a new internal connection on a device with attenuation. This does
+     * not check that the connection is actually valid (e.g. an input port to an
+     * output port, attenuation if within the acceptable range).
+     *
+     * Connections are represented as flows with an input port, output port, and
+     * channel. Implementation of attenuation is up to the vendor.
+     *
+     * @param deviceId DeviceId of the device to create this connection for
+     * @param priority priority of the flow
+     * @param isPermanent permanence of the flow
+     * @param timeout timeout in seconds
+     * @param inPort input port
+     * @param outPort output port
+     * @param ochSignal channel to use
+     * @param attenuation attenuation of the connection
+     * @return FlowId of the FlowRule representing the connection
+     */
+    FlowId createConnection(DeviceId deviceId, int priority, boolean isPermanent,
+                          int timeout, PortNumber inPort, PortNumber outPort,
+                          OchSignal ochSignal, long attenuation);
+
+    /**
+     * Removes an internal connection from a device by matching the FlowId and
+     * removing the flow representing the connection. This will remove any flow
+     * from any device so FlowId should correspond with a connection flow.
+     *
+     * @param deviceId DeviceId of the device to remove the connection from
+     * @param flowId FlowId of the flow representing the connection to remove
+     */
+    void removeConnection(DeviceId deviceId, FlowId flowId);
+
+    /**
+     * Returns true if the target power for this port can be configured.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of the port to check
+     * @return true if the target power for this port can be configured, false
+     * otherwise
+     */
+    boolean hasPortTargetPower(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Returns true if value is within the acceptable target power range of the port.
+     * Returns false if the port does not have a configurable target
+     * power.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param portNumber PortNumber of the port to check
+     * @param power value to check
+     * @return true if value is within the acceptable target power range, false
+     * otherwise
+     */
+    boolean portTargetPowerInRange(DeviceId deviceId, PortNumber portNumber, long power);
+
+    /**
+     * Returns true if value is within the acceptable attenuation range of a
+     * connection, and always returns false if the connection does not support
+     * attenuation. The attenuation range is determined by either the input
+     * or output port of the connection.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param portNumber PortNumber of either the input or output port of the connection
+     * @param att value to check
+     * @return true if value is within the acceptable attenuation range, false
+     * otherwise
+     */
+    boolean attenuationInRange(DeviceId deviceId, PortNumber portNumber, long att);
+
+    /**
+     * Returns true if the port is an input port.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param portNumber PortNumber of the port to check
+     * @return true if the port is an input port, false otherwise
+     */
+    boolean validInputPort(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Returns true if the port is an output port.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param portNumber PortNumber of the port to check
+     * @return true if the port is an output port, false otherwise
+     */
+    boolean validOutputPort(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Returns true if the channel is supported by the port. The port can be either
+     * an input or output port.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param portNumber PortNumber of the port to check
+     * @param ochSignal channel to check
+     * @return true if the channel is supported by the port, false otherwise
+     */
+    boolean validChannel(DeviceId deviceId, PortNumber portNumber, OchSignal ochSignal);
+
+    /**
+     * Returns true if the channel is not being used by a connection on the
+     * device.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param ochSignal channel to check
+     * @return true if the channel is not in use, false otherwise
+     */
+    boolean channelAvailable(DeviceId deviceId, OchSignal ochSignal);
+
+    /**
+     * Returns true if the connection from the input port to the output port is
+     * valid. This currently only checks if the given input and output ports are,
+     * respectively, valid input and output ports.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @param inPort input port of the connection
+     * @param outPort output port of the connection
+     * @return true if the connection is valid, false otherwise
+     */
+    boolean validConnection(DeviceId deviceId, PortNumber inPort, PortNumber outPort);
+
+    /**
+     * Returns the acceptable target port power range for a port.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of the port
+     * @return range if found, null otherwise
+     */
+    Range<Long> targetPortPowerRange(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Returns the acceptable attenuation range for a connection.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of either the input or output port
+     * @param ochSignal channel to check
+     * @return range if found, null otherwise
+     */
+    Range<Long> attenuationRange(DeviceId deviceId, PortNumber portNumber,
+                                 OchSignal ochSignal);
+
+    /**
+     * Returns the expected input power range for an input port.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of an input port
+     * @return range if found, null otherwise
+     */
+    Range<Long> inputPortPowerRange(DeviceId deviceId, PortNumber portNumber);
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/RoadmStore.java b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmStore.java
new file mode 100644
index 0000000..202ba8d
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/RoadmStore.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+package org.onosproject.roadm;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Interface for the ROADM store. Currently used to store target power only.
+ * This should be removed if target power could be read port annotations.
+ */
+public interface RoadmStore {
+
+    /**
+     * Adds the device to the store.
+     *
+     * <p>The device needs to be added to the store
+     * before setTargetPower and getTargetPower can be used. This does not initialize
+     * any of the target powers.
+     *
+     * @param deviceId DeviceId of the device to add
+     */
+    void addDevice(DeviceId deviceId);
+
+    /**
+     * Returns true if the device has been added to the store.
+     *
+     * @param deviceId DeviceId of the device to check
+     * @return true if device has been added to the store, false otherwise
+     */
+    boolean deviceAvailable(DeviceId deviceId);
+
+    /**
+     * Stores the targetPower for a port on a device. The device needs to be added
+     * to the store before this can be called. This does nothing if the device is
+     * not added.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of the port
+     * @param targetPower target port power to store
+     */
+    void setTargetPower(DeviceId deviceId, PortNumber portNumber, long targetPower);
+
+    /**
+     * Returns the targetPower for a port on a device. The device needs to be added
+     * to the store before this can be called. Returns null if the port's target
+     * power has not yet been initialized using setTargetPower.
+     *
+     * @param deviceId DeviceId of the device
+     * @param portNumber PortNumber of the port
+     * @return target power if target power has already been set, null otherwise
+     */
+    Long getTargetPower(DeviceId deviceId, PortNumber portNumber);
+}
diff --git a/apps/roadm/src/main/java/org/onosproject/roadm/package-info.java b/apps/roadm/src/main/java/org/onosproject/roadm/package-info.java
new file mode 100644
index 0000000..156b44a
--- /dev/null
+++ b/apps/roadm/src/main/java/org/onosproject/roadm/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * Application to monitor and configure ROADM devices.
+ */
+package org.onosproject.roadm;
diff --git a/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.css b/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.css
new file mode 100644
index 0000000..80f975f
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.css
@@ -0,0 +1,39 @@
+/* css for ROADM device table view */
+
+.less-gap {
+    margin-top: -20px;
+}
+
+#ov-roadm-device h2 {
+    display: inline-block;
+}
+
+/* Panel Styling */
+#ov-roadm-device-item-details-panel.floatpanel {
+    position: absolute;
+    top: 115px;
+}
+
+.light #ov-roadm-device-item-details-panel.floatpanel {
+    background-color: rgb(229, 234, 237);
+}
+.dark #ov-roadm-device-item-details-panel.floatpanel {
+    background-color: #3A4042;
+}
+
+#ov-roadm-device-item-details-panel h3 {
+    margin: 0;
+    font-size: large;
+}
+
+#ov-roadm-device-item-details-panel h4 {
+    margin: 0;
+}
+
+#ov-roadm-device-item-details-panel td {
+    padding: 5px;
+}
+#ov-roadm-device-item-details-panel td.label {
+    font-style: italic;
+    opacity: 0.8;
+}
diff --git a/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.html b/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.html
new file mode 100644
index 0000000..a9a70e8
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.html
@@ -0,0 +1,70 @@
+<!-- partial HTML -->
+<div id="ov-roadm-device" class="less-gap">
+
+    <div class="tabular-header">
+        <h2>Optical Devices ({{tableData.length}} total)</h2>
+        <div class="ctrl-btns">
+            <div class="refresh" ng-class="{active: autoRefresh}"
+                 icon icon-id="refresh" icon-size="42"
+                 tooltip tt-msg="autoRefreshTip"
+                 ng-click="toggleRefresh()"></div>
+            <div class="separator"></div>
+
+            <div ng-class="{'current-view': !!selId}"
+                 icon icon-id="deviceTable" icon-size="42"></div>
+
+            <div ng-class="{active: !!selId}"
+                 icon icon-id="flowTable" icon-size="42"
+                 tooltip tt-msg="flowTip"
+                 ng-click="nav('roadmFlow')"></div>
+
+
+            <div ng-class="{active: !!selId}"
+                 icon icon-id="portTable" icon-size="42"
+                 tooltip tt-msg="portTip"
+                 ng-click="nav('roadmPort')"></div>
+        </div>
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+
+        <div class="table-header" onos-sortable-header>
+            <table>
+                <tr>
+                    <td colId="name"sortable>Friendly Name</td>
+                    <td colId="id" sortable>Device ID </td>
+                    <td colId="master" sortable col-width="120px">Master </td>
+                    <td colId="ports" sortable col-width="70px">Ports </td>
+                    <td colId="vendor" sortable>Vendor </td>
+                    <td colId="hwVersion" sortable>H/W Version </td>
+                    <td colId="swVersion" sortable>S/W Version </td>
+                    <td colId="protocol" sortable col-width="100px">Protocol </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr ng-if="!tableData.length" class="no-data">
+                    <td colspan="3">
+                        {{annots.no_rows_msg}}
+                    </td>
+                </tr>
+
+                <tr ng-repeat="item in tableData track by $index"
+                    ng-class="{selected: item.id === selId}"
+                    ng-click="selectCallback($event, item)">
+                    <td>{{item.name}}</td>
+                    <td>{{item.id}}</td>
+                    <td>{{item.master}}</td>
+                    <td>{{item.ports}}</td>
+                    <td>{{item.vendor}}</td>
+                    <td>{{item.hwVersion}}</td>
+                    <td>{{item.swVersion}}</td>
+                    <td>{{item.protocol}}</td>
+                </tr>
+            </table>
+        </div>
+
+    </div>
+</div>
diff --git a/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.js b/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.js
new file mode 100644
index 0000000..2537ed8
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmDevice/roadmDevice.js
@@ -0,0 +1,49 @@
+// js for roadm device table view
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log, $scope, $loc, wss, ns;
+
+    // constants
+    var detailsReq = 'roadmDeviceDetailsRequest';
+
+    angular.module('ovRoadmDevice', [])
+        .controller('OvRoadmDeviceCtrl',
+        ['$log', '$scope', '$location', 'TableBuilderService', 'WebSocketService',
+            'NavService',
+
+            function (_$log_, _$scope_, _$loc_, tbs, _wss_, _ns_) {
+                $log = _$log_;
+                $scope = _$scope_;
+                $loc = _$loc_;
+                wss = _wss_;
+                ns = _ns_;
+
+                // query for if a certain device needs to be highlighted
+                var params = $loc.search();
+                if (params.hasOwnProperty('devId')) {
+                    $scope.selId = params['devId'];
+                }
+
+                // TableBuilderService creating a table for us
+                tbs.buildTable({
+                    scope: $scope,
+                    tag: 'roadmDevice'
+                });
+
+                $scope.nav = function (path) {
+                    if ($scope.selId) {
+                        ns.navTo(path, { devId: $scope.selId });
+                    }
+                };
+
+                // cleanup
+                $scope.$on('$destroy', function () {
+                    //wss.unbindHandlers(handlers);
+                    $log.log('OvRoadmDeviceCtrl has been destroyed');
+                });
+
+                $log.log('OvRoadmDeviceCtrl has been created');
+            }]);
+}());
diff --git a/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.css b/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.css
new file mode 100644
index 0000000..81e5c16
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.css
@@ -0,0 +1,132 @@
+/* css for ROADM flow table view */
+
+#ov-roadm-flow h2 {
+    display: inline-block;
+}
+
+/* Panel Styling */
+#ov-roadm-flow-item-details-panel.floatpanel {
+    position: absolute;
+    top: 115px;
+}
+
+.light #ov-roadm-flow-item-details-panel.floatpanel {
+    background-color: rgb(229, 234, 237);
+}
+
+.dark #ov-roadm-flow-item-details-panel.floatpanel {
+    background-color: #3A4042;
+}
+
+#ov-roadm-flow-item-details-panel h3 {
+    margin: 0;
+    font-size: large;
+}
+
+#ov-roadm-flow-item-details-panel h4 {
+    margin: 0;
+}
+
+#ov-roadm-flow-item-details-panel td {
+    padding: 5px;
+}
+#ov-roadm-flow-item-details-panel td.label {
+    font-style: italic;
+    opacity: 0.8;
+}
+
+#ov-roadm-flow .table-header span.units {
+    font-variant: normal;
+    text-transform: none;
+}
+
+/* editable attenuation */
+#ov-roadm-flow .editable span {
+    width: 100%;
+    display: inline-block;
+}
+
+#ov-roadm-flow .editable span.attenuation:hover {
+    color: #009fdb
+}
+
+#ov-roadm-flow .editable input {
+    padding: 0;
+}
+#ov-roadm-flow .editable button {
+    margin: 0;
+    padding: 0px 5px 0px 5px;
+}
+
+#ov-roadm-flow .editable input {
+    width: 80px;
+}
+
+#ov-roadm-flow .editable .input-error {
+    color: red;
+    font-size: 10px;
+    width: 180px;
+}
+
+/* delete flow button */
+#ov-roadm-flow .table-body .delete-icon {
+    font-size: 24px;
+    line-height: 0px;
+    text-align: center;
+}
+
+#ov-roadm-flow .table-body .delete-icon:hover {
+    color: red;
+}
+
+/* Create connection form */
+#ov-roadm-flow div.flow-form {
+    background-color: #ffffff;
+    border: 1px solid #888888;
+    width: 720px;
+    height: 270px;
+    padding: 20px;
+    position: absolute;
+    right: 15px;
+    bottom: 15px;
+}
+
+#ov-roadm-flow .flow-form div.delete-icon {
+    cursor: pointer;
+    cursor: hand;
+    font-size: 36px;
+    position: absolute;
+    right: 20px;
+    top: 5px;
+}
+
+#ov-roadm-flow .flow-form label {
+    width: 150px;
+    display: inline-block;
+}
+
+#ov-roadm-flow .flow-form input {
+    width: 150px;
+    display: inline-block
+}
+
+#ov-roadm-flow .flow-form select {
+    width: 150px;
+}
+
+#ov-roadm-flow .flow-form .form-error {
+    margin-left: 15px;
+    color: red;
+}
+
+#ov-roadm-flow .flow-form form {
+    font-size: 14px;
+    color: #444444;
+    line-height: 26px;
+    margin-bottom: 15px;
+}
+
+#ov-roadm-flow .flow-form button.submit {
+    margin-left: 150px;
+    width: 150px;
+}
diff --git a/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.html b/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.html
new file mode 100644
index 0000000..323eec5
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.html
@@ -0,0 +1,118 @@
+<!-- partial HTML -->
+<div id="ov-roadm-flow" class="less-gap">
+
+    <div class="tabular-header">
+        <h2>Connections for Optical Device {{devId}} ({{tableData.length}} total)</h2>
+        <div class="ctrl-btns">
+            <div class="active"
+                 icon icon-id="plus" icon-size="42"
+                 tooltip tt-msg="addFlowTip"
+                 ng-click="displayFlowForm()"></div>
+
+            <div class="refresh" ng-class="{active: autoRefresh}"
+                 icon icon-id="refresh" icon-size="42"
+                 tooltip tt-msg="autoRefreshTip"
+                 ng-click="toggleRefresh()"></div>
+            <div class="separator"></div>
+
+            <div class="active"
+                 icon icon-id="deviceTable" icon-size="42"
+                 tooltip tt-msg="deviceTip"
+                 ng-click="nav('roadmDevice')"></div>
+
+            <div class="current-view"
+                 icon icon-id="flowTable" icon-size="42"
+                 tooltip tt-msg="flowTip"></div>
+
+            <div class="active"
+                 icon icon-id="portTable" icon-size="42"
+                 tooltip tt-msg="portTip"
+                 ng-click="nav('roadmPort')"></div>
+        </div>
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+
+        <div class="table-header" onos-sortable-header>
+            <table>
+                <tr>
+                    <td col-width="30px"></td>
+                    <td colId="flowId" sortable col-width="180px">Flow ID </td>
+                    <td colId="appId" sortable>App ID </td>
+                    <td colId="priority" sortable>Priority </td>
+                    <td colId="timeout" sortable>Timeout </td>
+                    <td colId="permanent" sortable>Permanent </td>
+                    <td colId="state" sortable>State </td>
+                    <td colId="inPort" sortable>In Port </td>
+                    <td colId="outPort" sortable>Out Port </td>
+                    <td colId="multiplier" sortable>Channel </td>
+                    <td colId="spacing">Spacing <span class="units">(GHz)</span> </td>
+                    <td colId="currentPower" col-width="180px">Current Power <span class="units">(0.01dBm)</span></td>
+                    <td colId="attenuation" col-width="200px">Attenuation <span class="units">(0.01dB)</span></td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr ng-if="!tableData.length" class="no-data">
+                    <td colspan="13">
+                        {{annots.no_rows_msg}}
+                    </td>
+                </tr>
+
+                <tr ng-repeat="flow in tableData track by $index"
+                    ng-class="{selected: flow.id === selId}">
+                    <td class="delete-icon" ng-click="deleteFlow($event, flow)">&#215;</td>
+                    <td>{{flow.flowId}}</td>
+                    <td>{{flow.appId}}</td>
+                    <td>{{flow.priority}}</td>
+                    <td>{{flow.timeout}}</td>
+                    <td>{{flow.permanent}}</td>
+                    <td>{{flow.state}}</td>
+                    <td>{{flow.inPort}}</td>
+                    <td>{{flow.outPort}}</td>
+                    <td>{{flow.multiplier}} ({{flow.multiplier * 0.05 + 193.1 | number:2}} THz)</td>
+                    <td>{{flow.spacing}}</td>
+                    <td>{{flow.currentPower}}</td>
+                    <td class="editable" roadm-att="flow" roadm-set-att="setAttenuation(flow, targetVal, cb)"></td>
+                </tr>
+            </table>
+        </div>
+
+    </div>
+
+    <div class="flow-form" ng-controller="FlowFormController as form" ng-show="showFlowForm">
+        <div class="delete-icon" ng-click="hideFlowForm()">&#215;</div>
+        <form>
+            <label>Priority</label><input type="number" ng-model="form.flow.priority" />
+            <span class="form-error" ng-show="form.priorityError" >{{form.priorityMessage}}</span><br />
+
+            <label>Is Permanent</label><input type="checkbox" ng-model="form.flow.permanent" /><br />
+
+            <label>Timeout</label><input type="number" ng-model="form.flow.timeout" ng-disabled="form.flow.permanent"/>
+            <span class="form-error" ng-show="form.timeoutError" >{{form.timeoutMessage}}</span><br />
+
+            <label>In Port</label><input type="number" ng-model="form.flow.inPort" />
+            <span class="form-error" ng-show="form.inPortError">{{form.inPortMessage}}</span><br />
+
+            <label>Out Port</label><input type="number" ng-model="form.flow.outPort" />
+            <span class="form-error" ng-show="form.outPortError">{{form.outPortMessage}}</span>
+            <span class="form-error" ng-show="form.connectionError">{{form.connectionMessage}}</span><br />
+
+            <label>Channel Spacing</label><select ng-model="form.flow.spacing" ng-options="x.freq for x in form.spacings"></select>
+            <span class="form-error" ng-show="form.spacingError">{{form.spacingMessage}}</span><br />
+
+            <label>Spacing Multiplier</label><input type="number" ng-model="form.flow.multiplier" />
+            <span class="form-error" ng-show="form.multiplierError">{{form.multiplierMessage}}</span>
+            <span class="form-error" ng-show="form.channelError">{{form.channelMessage}}</span><br />
+
+            <label>Include Attenuation</label><input type="checkbox" ng-model="form.flow.includeAttenuation" /><br />
+
+            <label>Attenuation</label><input type="number" ng-model="form.flow.attenuation" ng-disabled="!form.flow.includeAttenuation"/>
+            <span class="form-error" ng-show="form.attenuationError">{{form.attenuationMessage}}</span><br />
+        </form>
+        <button type="submit" class="submit" ng-click="form.createFlow(form.flow)">Create Connection</button>
+    </div>
+
+</div>
diff --git a/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.js b/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.js
new file mode 100644
index 0000000..ad9bf76e
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmFlow/roadmFlow.js
@@ -0,0 +1,324 @@
+// js for roadm flow table view
+(function () {
+    'use strict';
+
+    var SET_ATT_REQ = "roadmSetAttenuationRequest";
+    var SET_ATT_RESP = "roadmSetAttenuationResponse";
+    var DELETE_FLOW_REQ = "roadmDeleteFlowRequest";
+    var CREATE_FLOW_REQ = "roadmCreateFlowRequest";
+    var CREATE_FLOW_RESP = "roadmCreateFlowResponse";
+
+    // injected references
+    var $log, $scope, $location, fs, tbs, wss, ns;
+
+    // used to map id to a request call function
+    var flowCbTable = {};
+
+    function setAttenuation(flow, targetVal, cb) {
+        flowCbTable[flow.id] = cb;
+        wss.sendEvent(SET_ATT_REQ,
+            {
+                devId: $scope.devId,
+                flowId: flow.id,
+                attenuation: targetVal
+            });
+    }
+
+    function attenuationCb(data) {
+        flowCbTable[data.flowId](data.valid, data.message);
+    }
+
+    // check if value is an integer
+    function isInteger(val) {
+        var INTEGER_REGEXP = /^\-?\d+$/;
+        if (INTEGER_REGEXP.test(val)) {
+            return true;
+        }
+        return false;
+    }
+
+    angular.module('ovRoadmFlow', [])
+    .controller('OvRoadmFlowCtrl',
+        ['$log', '$scope', '$location',
+            'FnService', 'TableBuilderService', 'WebSocketService', 'NavService',
+
+        function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _wss_, _ns_) {
+            var params;
+            $log = _$log_;
+            $scope = _$scope_;
+            $location = _$location_;
+            fs = _fs_;
+            tbs = _tbs_;
+            wss = _wss_;
+            ns = _ns_;
+
+            $scope.addFlowTip = 'Create a flow';
+            $scope.deviceTip = 'Show device table';
+            $scope.flowTip = 'Show flow view for this device';
+            $scope.groupTip = 'Show group view for this device';
+            $scope.meterTip = 'Show meter view for selected device';
+
+            $scope.showFlowForm = false;
+
+            var handlers = {};
+            handlers[SET_ATT_RESP] = attenuationCb;
+            wss.bindHandlers(handlers);
+
+            params = $location.search();
+            if (params.hasOwnProperty('devId')) {
+                $scope.devId = params['devId'];
+            }
+
+            tbs.buildTable({
+                scope: $scope,
+                tag: 'roadmFlow',
+                query: params
+            });
+
+            $scope.displayFlowForm = function () {
+                $scope.showFlowForm = true;
+            }
+
+            $scope.hideFlowForm = function () {
+                $scope.showFlowForm = false;
+            }
+
+            $scope.setAttenuation = setAttenuation;
+
+            $scope.deleteFlow = function ($event, row) {
+                wss.sendEvent(DELETE_FLOW_REQ,
+                    {
+                        devId: $scope.devId,
+                        id: row.id
+                    });
+            }
+
+            $scope.createFlow = function(flow) {
+                wss.sendEvent(CREATE_FLOW_REQ,
+                    {
+                        devId: $scope.devId,
+                        flow: flow
+                    });
+            }
+
+            $scope.fakeCurrentPower = function(flow) {
+                if (!isNaN(flow.currentPower)) {
+                    var val = parseInt(flow.attenuation);
+                    return val + (val % 5 - 2);
+                } else {
+                    return flow.currentPower;
+                }
+            }
+
+            $scope.nav = function (path) {
+                if ($scope.devId) {
+                    ns.navTo(path, { devId: $scope.devId });
+                }
+            };
+
+            $scope.$on('$destroy', function () {
+                wss.unbindHandlers(handlers);
+            });
+
+            $log.log('OvRoadmFlowCtrl has been created');
+        }])
+
+    .directive('roadmAtt', ['WebSocketService', function() {
+
+        var retTemplate =
+            '<span class="attenuation" ng-show="!editMode" ng-click="enableEdit()">{{currItem.attenuation}}</span>' +
+            '<form ng-show="editMode" name="form" novalidate>' +
+                '<input type="number" name="formVal" ng-model="formVal">' +
+                '<button type="submit" class="submit" ng-click="send()">Set</button>' +
+                '<button type="button" class="cancel" ng-click="cancel()">Cancel</button>' +
+                '<span class="input-error" ng-show="showError">{{errorMessage}}</span>' +
+            '</form>';
+
+        return {
+            restrict: 'A',
+            scope: {
+                currItem: '=roadmAtt',
+                roadmSetAtt: '&'
+            },
+            template: retTemplate,
+            link: function ($scope, $element) {
+                $scope.editMode = false;
+                $scope.showError = false;
+                $scope.errorMessage = "Invalid attenuation"
+            },
+            controller: function($scope, $timeout) {
+                $scope.enableEdit = function() {
+                    // connection must support attenuation to be editable
+                    if ($scope.editMode === false) {
+                        // Ensure that the entry being edited remains the same even
+                        // if the table entries are shifted around.
+                        $scope.targetItem = $scope.currItem;
+                        // Ensure the value seen in the field remains the same
+                        $scope.formVal = parseInt($scope.currItem.attenuation);
+                        $scope.editMode = true;
+                        $timeout(function () {
+                            $scope.$apply()
+                        });
+                    }
+                };
+                $scope.send = function() {
+                    // check input is an integer
+                    if (!isInteger($scope.formVal)) {
+                        $scope.sendCb(false, "Attenuation must be an integer");
+                        return;
+                    }
+                    $scope.roadmSetAtt({flow: $scope.targetItem, targetVal: $scope.formVal, cb: $scope.sendCb});
+                };
+                // Callback for server-side validation. Displays the error message
+                // if the input is invalid.
+                $scope.sendCb = function(valid, message) {
+                    if (valid) {
+                        // check if it's still pointing to the same item
+                        // reordering the entries may change the binding
+                        if ($scope.currItem.id === $scope.targetItem.id) {
+                            // update the ui to display the new attenuation value
+                            $scope.currItem.attenuation = $scope.formVal;
+                        }
+                        $scope.cancel();
+                    } else {
+                        $scope.errorMessage = message;
+                        $scope.showError = true;
+                    }
+                    $timeout(function () {
+                        $scope.$apply()
+                    });
+                }
+                $scope.cancel = function() {
+                    $scope.editMode = false;
+                    $scope.showError = false;
+                }
+            }
+        };
+    }])
+
+    .controller('FlowFormController', function($timeout) {
+        var notIntegerError = "Must be an integer.";
+
+        this.clearErrors = function() {
+            this.priorityError = false;
+            this.timeoutError = false;
+            this.isPermanentError = false;
+            this.inPortError = false;
+            this.outPortError = false;
+            this.spacingError = false;
+            this.multiplierError = false;
+            this.attenuationError = false;
+            this.connectionError = false;
+            this.channelError = false;
+        }
+        this.clearErrors();
+
+        this.spacings = [
+            {index: 0, freq: "100 GHz"},
+            {index: 1, freq: "50 GHz"},
+            {index: 2, freq: "25 GHz"},
+            {index: 3, freq: "12.5 GHz"}
+        ];
+
+        this.flow = {};
+        //this.flow.priority = 88;
+        this.flow.permanent = true;
+        this.flow.timeout = 0;
+        //this.flow.inPort = 2;
+        //this.flow.outPort = 2;
+        this.flow.spacing = this.spacings[1];
+        //this.flow.multiplier = 0;
+        this.flow.includeAttenuation = true;
+        this.flow.attenuation = 0;
+
+        var parent = this;
+
+        function createFlowCb(data) {
+            if (!data.inPort.valid) {
+                parent.inPortMessage = data.inPort.message;
+                parent.inPortError = true;
+            }
+            if (!data.outPort.valid) {
+                parent.outPortMessage = data.outPort.message;
+                parent.outPortError = true;
+            }
+            if (!data.connection.valid) {
+                parent.connectionMessage = data.connection.message;
+                parent.connectionError = true;
+            }
+            if (!data.spacing.valid) {
+                parent.spacingMessage = data.spacing.message;
+                parent.spacingError = true;
+            }
+            if (!data.multiplier.valid) {
+                parent.multiplierMessage = data.multiplier.message;
+                parent.multiplierError = true;
+            }
+            if (!data.channelAvailable.valid) {
+                parent.channelMessage = data.channelAvailable.message;
+                parent.channelError = true;
+            }
+            if (data.includeAttenuation && !data.attenuation.valid) {
+                parent.attenuationMessage = data.attenuation.message;
+                parent.attenuationError = true;
+            }
+            $timeout(function () {
+                $scope.$apply()
+            });
+        }
+
+        var handlers = {}
+        handlers[CREATE_FLOW_RESP] = createFlowCb;
+        wss.bindHandlers(handlers);
+
+        this.createFlow = function(connection) {
+            this.clearErrors();
+
+            var error = false;
+            if (!isInteger(connection.priority)) {
+                this.priorityMessage = notIntegerError;
+                this.priorityError = true;
+                error = true;
+            }
+            if (!connection.permanent && !isInteger(connection.timeout)) {
+                this.timeoutMessage = notIntegerError;
+                this.timeoutError = true;
+                error = true;
+            }
+            if (!isInteger(connection.inPort)) {
+                this.inPortMessage = notIntegerError;
+                this.inPortError = true;
+                error = true;
+            }
+            if (!isInteger(connection.outPort)) {
+                this.outPortMessage = notIntegerError;
+                this.outPortError = true;
+                error = true;
+            }
+            if (!isInteger(connection.multiplier)) {
+                this.multiplierMessage = notIntegerError;
+                this.multiplierError = true;
+                error = true;
+            }
+            if (connection.includeAttenuation && !isInteger(connection.attenuation)) {
+                this.attenuationMessage = notIntegerError;
+                this.attenuationError = true;
+                error = true;
+            }
+
+            if (!error) {
+                wss.sendEvent(CREATE_FLOW_REQ,
+                    {
+                        devId: $scope.devId,
+                        formData: connection
+                    });
+                $log.log('Request to create connection has been sent');
+            }
+        }
+
+        $scope.$on('$destroy', function () {
+            wss.unbindHandlers(handlers);
+        });
+    });
+
+}());
diff --git a/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.css b/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.css
new file mode 100644
index 0000000..511b2fc
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.css
@@ -0,0 +1,69 @@
+/* css for ROADM port table view */
+
+#ov-roadm-port h2 {
+    display: inline-block;
+}
+
+/* Panel Styling */
+#ov-roadm-port-item-details-panel.floatpanel {
+    position: absolute;
+    top: 115px;
+}
+
+.light #ov-roadm-port-item-details-panel.floatpanel {
+    background-color: rgb(229, 234, 237);
+}
+.dark #ov-roadm-port-item-details-panel.floatpanel {
+    background-color: #3A4042;
+}
+
+#ov-roadm-port-item-details-panel h3 {
+    margin: 0;
+    font-size: large;
+}
+
+#ov-roadm-port-item-details-panel h4 {
+    margin: 0;
+}
+
+#ov-roadm-port-item-details-panel td {
+    padding: 5px;
+}
+
+#ov-roadm-port-item-details-panel td.label {
+    font-style: italic;
+    opacity: 0.8;
+}
+
+#ov-roadm-port .table-header span.units {
+    font-variant: normal;
+    text-transform: none;
+}
+
+/* Editable Target Power field */
+#ov-roadm-port .editable span {
+    width: 100%;
+    display: inline-block;
+}
+
+#ov-roadm-port .editable span.target-power:hover {
+    color: #009fdb
+}
+
+#ov-roadm-port .editable input {
+    padding: 0;
+}
+#ov-roadm-port .editable button {
+    margin: 0;
+    padding: 0px 5px 0px 5px;
+}
+
+#ov-roadm-port .editable input {
+    width: 80px;
+}
+
+#ov-roadm-port .editable .input-error {
+    color: red;
+    font-size: 10px;
+    width: 180px;
+}
diff --git a/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.html b/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.html
new file mode 100644
index 0000000..1e2affd
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.html
@@ -0,0 +1,73 @@
+<!-- partial HTML -->
+<div id="ov-roadm-port" class="less-gap">
+
+    <div class="tabular-header">
+        <h2>Ports for Optical Device {{devId}} ({{tableData.length}} total)</h2>
+        <div class="ctrl-btns">
+            <div class="refresh" ng-class="{active: autoRefresh}"
+                 icon icon-id="refresh" icon-size="42"
+                 tooltip tt-msg="autoRefreshTip"
+                 ng-click="toggleRefresh()"></div>
+            <div class="separator"></div>
+
+            <div class="active"
+                 icon icon-id="deviceTable" icon-size="42"
+                 tooltip tt-msg="deviceTip"
+                 ng-click="nav('roadmDevice')"></div>
+
+            <div class="active"
+                 icon icon-id="flowTable" icon-size="42"
+                 tooltip tt-msg="flowTip"
+                 ng-click="nav('roadmFlow')"></div>
+
+            <div class="current-view"
+                 icon icon-id="portTable" icon-size="42"
+                 tooltip tt-msg="portTip"></div>
+        </div>
+    </div>
+
+    <div class="summary-list" onos-table-resize>
+
+        <div class="table-header" onos-sortable-header>
+            <table>
+                <tr>
+                    <td colId="id" sortable>Port Number </td>
+                    <td colId="name" sortable>Name </td>
+                    <td colId="type" sortable>Type </td>
+                    <td colId="enabled" sortable>Enabled </td>
+                    <td colId="minFreq" sortable>Min Freq <span class="units">(THz)</span> </td>
+                    <td colId="maxFreq" sortable>Max Freq <span class="units">(THz)</span> </td>
+                    <td colId="grid" sortable>Grid <span class="units">(GHz)</span> </td>
+                    <td colId="portMac" sortable>Input Power Range </td>
+                    <td colId="currentPower">Current Power <span class="units">(0.01dBm)</span> </td>
+                    <td colId="targetPower" col-width="200px">Target Power <span class="units">(0.01dBm)</span> </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table>
+                <tr ng-if="!tableData.length" class="no-data">
+                    <td colspan="10">
+                        {{annots.no_rows_msg}}
+                    </td>
+                </tr>
+
+                <tr ng-repeat="item in tableData track by $index"
+                    ng-class="{selected: item.id === selId}">
+                    <td>{{item.id}}</td>
+                    <td>{{item.name}}</td>
+                    <td>{{item.type}}</td>
+                    <td>{{item.enabled}}</td>
+                    <td>{{item.minFreq}}</td>
+                    <td>{{item.maxFreq}}</td>
+                    <td>{{item.grid}}</td>
+                    <td>{{item.inputPowerRange}}</td>
+                    <td>{{item.currentPower}}</td>
+                    <td class="editable" roadm-power="item" roadm-set-power="setPortPower(port, targetVal, cb)"></td>
+                </tr>
+            </table>
+        </div>
+
+    </div>
+</div>
diff --git a/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.js b/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.js
new file mode 100644
index 0000000..447ac7d
--- /dev/null
+++ b/apps/roadm/src/main/resources/app/view/roadmPort/roadmPort.js
@@ -0,0 +1,168 @@
+// js for roadm port table view
+(function () {
+    'use strict';
+
+    var SET_TARGET_POWER_REQ = "roadmSetTargetPowerRequest";
+    var SET_TARGET_POWER_RESP = "roadmSetTargetPowerResponse";
+
+    // injected references
+    var $log, $scope, $location, fs, tbs, wss, ns;
+
+    var portCbTable = {};
+
+    function setPortPower(port, targetVal, cb) {
+        var id = port.id;
+        portCbTable[id] = cb;
+        wss.sendEvent("roadmSetTargetPowerRequest",
+            {
+                devId: $scope.devId,
+                id: port.id,
+                targetPower: targetVal
+            });
+    }
+
+    function portPowerCb(data) {
+        portCbTable[data.id](data.valid, data.message);
+    }
+
+    // check if value is an integer
+    function isInteger(val) {
+        var INTEGER_REGEXP = /^\-?\d+$/;
+        if (INTEGER_REGEXP.test(val)) {
+            return true;
+        }
+        return false;
+    }
+
+    angular.module('ovRoadmPort', [])
+    .controller('OvRoadmPortCtrl',
+        ['$log', '$scope', '$location',
+            'FnService', 'TableBuilderService', 'WebSocketService', 'NavService',
+
+        function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _wss_, _ns_) {
+            var params;
+            $log = _$log_;
+            $scope = _$scope_;
+            $location = _$location_;
+            fs = _fs_;
+            tbs = _tbs_;
+            wss = _wss_;
+            ns = _ns_;
+
+            $scope.deviceTip = 'Show device table';
+            $scope.flowTip = 'Show flow view for this device';
+            $scope.groupTip = 'Show group view for this device';
+            $scope.meterTip = 'Show meter view for selected device';
+
+            var handlers = {};
+            handlers[SET_TARGET_POWER_RESP] = portPowerCb;
+            wss.bindHandlers(handlers);
+
+            params = $location.search();
+            if (params.hasOwnProperty('devId')) {
+                $scope.devId = params['devId'];
+            }
+
+            tbs.buildTable({
+                scope: $scope,
+                tag: 'roadmPort',
+                query: params
+            });
+
+            $scope.setPortPower = setPortPower;
+
+            $scope.setTargetPower = function (port, targetVal) {
+                wss.sendEvent("roadmSetTargetPowerRequest",
+                    {
+                        devId: $scope.devId,
+                        id: port.id,
+                        targetPower: targetVal
+                    });
+                $log.debug('Got a click on:', port);
+            }
+
+            $scope.nav = function (path) {
+                if ($scope.devId) {
+                    ns.navTo(path, { devId: $scope.devId });
+                }
+            };
+
+            $scope.$on('$destroy', function () {
+                wss.unbindHandlers(handlers);
+            });
+
+            $log.log('OvRoadmPortCtrl has been created');
+        }])
+
+    .directive('roadmPower', ['WebSocketService', function() {
+
+        var retTemplate =
+            '<span class="target-power" ng-show="!editMode" ng-click="enableEdit()">{{currItem.targetPower}}</span>' +
+            '<form ng-show="editMode" name="form" novalidate>' +
+                '<input type="number" name="formVal" ng-model="formVal">' +
+                '<button type="submit" ng-click="send()">Set</button>' +
+                '<button type="button" ng-click="cancel()">Cancel</button>' +
+                '<span class="input-error" ng-show="showError">{{errorMessage}}</span>' +
+            '</form>';
+
+        return {
+            restrict: 'A',
+            scope: {
+                currItem: '=roadmPower',
+                roadmSetPower: '&'
+            },
+            template: retTemplate,
+            link: function ($scope, $element) {
+                $scope.editMode = false;
+                $scope.showError = false;
+                $scope.errorMessage = "Invalid target power";
+            },
+            controller: function($scope, $timeout) {
+                $scope.enableEdit = function() {
+                    if ($scope.currItem.hasTargetPower === "true" && $scope.editMode === false) {
+                        // Ensure that the entry being edited remains the same even
+                        // if the table entries are shifted around.
+                        $scope.targetItem = $scope.currItem;
+                        // Ensure the value seen in the field remains the same
+                        $scope.formVal = parseInt($scope.currItem.targetPower);
+                        $scope.editMode = true;
+                        $timeout(function () {
+                            $scope.$apply()
+                        });
+                    }
+                };
+                // Callback for server-side validation. Displays the error message
+                // if the input is invalid.
+                $scope.sendCb = function(valid, message) {
+                    if (valid) {
+                        // check if it's still pointing to the same item
+                        // reordering the entries may change the binding
+                        if ($scope.currItem.id === $scope.targetItem.id) {
+                            // update the ui to display the new attenuation value
+                            $scope.currItem.targetPower = $scope.formVal;
+                        }
+                        $scope.cancel();
+                    } else {
+                        $scope.errorMessage = message;
+                        $scope.showError = true;
+                    }
+                    $timeout(function () {
+                        $scope.$apply()
+                    });
+                }
+                $scope.send = function() {
+                    // check input is an integer
+                    if (!isInteger($scope.formVal)) {
+                        $scope.sendCb(false, "Target power must be an integer");
+                        return;
+                    }
+                    $scope.roadmSetPower({port: $scope.targetItem, targetVal: $scope.formVal, cb: $scope.sendCb});
+                };
+                $scope.cancel = function() {
+                    $scope.editMode = false;
+                    $scope.showError = false;
+                }
+            }
+        };
+    }]);
+}());
diff --git a/apps/roadm/src/main/resources/webgui/css.html b/apps/roadm/src/main/resources/webgui/css.html
new file mode 100644
index 0000000..4db2eeb
--- /dev/null
+++ b/apps/roadm/src/main/resources/webgui/css.html
@@ -0,0 +1,3 @@
+<link rel="stylesheet" href="app/view/roadmDevice/roadmDevice.css">
+<link rel="stylesheet" href="app/view/roadmPort/roadmPort.css">
+<link rel="stylesheet" href="app/view/roadmFlow/roadmFlow.css">
diff --git a/apps/roadm/src/main/resources/webgui/js.html b/apps/roadm/src/main/resources/webgui/js.html
new file mode 100644
index 0000000..b3e37df
--- /dev/null
+++ b/apps/roadm/src/main/resources/webgui/js.html
@@ -0,0 +1,3 @@
+<script src="app/view/roadmDevice/roadmDevice.js"></script>
+<script src="app/view/roadmPort/roadmPort.js"></script>
+<script src="app/view/roadmFlow/roadmFlow.js"></script>